使用psd.js将PSD转成SVG -- 基础篇(图形)

做者:佛寺   方凳雅集出品

背景

随着发展,活动会场页面的题图运营须要线上模板化,而自研的导购素材制做平台接入了海棠-创意中心,经过平台能力,将素材模板化,而且经过配置化的方式生成多种场景化,个性化的素材。可是创意中心的素材模板是基于SVG的,而会场的题图基本是基于Photoshop(PS)输出,源文件是PSD。因为SVG是面向矢量图形的标记语言,而PS是以位图处理为中心的图像处理软件,大多时候,PS没法直接导出SVG文件。html

为了能让会场的题图模板接入到导购素材制做平台,同时下降设计师的使用门槛,咱们须要在导购素材制做平台中实现直接将PSD转成SVG的功能,在线化的将PSD转成SVG,而后导入到创意中心,将题图模板化。node

处理图形

在PS中,绘制图形通常会用到钢笔工具。bash



对于使用设计师而言,钢笔的运用是必备的技能,好比抠图、绘制图案、制做图标等都离不开钢笔工具。钢笔工具又能够叫路径工具,它输出的是一种矢量图,和位图不一样的是,矢量图能够保证输出的图案形状不会由于缩放变形而失真。app



SVG的全称又叫Scalable Vector Graphics,自己就是面向矢量图形的标记语言,因此,对于PSD中的图形路径的信息,理论上是能够映射到SVG中的。svg


在SVG中,用于显示图形的标签有不少:工具



若是是直接使用SVG输出图形的话,咱们可能须要根据形状来考虑用哪一个标签。好比圆形,咱们会有些考虑使用circle标签,矩形,咱们就会用rect,多边形,咱们会用polygon,这些标签能让咱们更加快速方便的绘制出想要的形状。但若是是要将PSD中的图形转换成SVG的话,就很差根据形状来选择合适的标签了,这样会使转换的实现变得复杂。post


咱们能不能将不一样图形的绘制都统一成一种解法呢?ui


能够的,那就是用path标签。它是SVG基本形状中最强大的一个,提供了一套绘制语法,不只能建立其余基本形状,还能建立更多其余形状。设计师不管是绘制什么形状,只要是用钢笔工具输出的,最终都会以路径节点的数据格式存储,经过psd.js获取到的图形信息,实际上就是一个图形路径节点的集合。this


获取路径节点


使用psd.js能够经过以下方式获取到图形路径的信息。spa


1const vectorMask = node.get('vectorMask');
2vectorMask.parse();
3const paths = vectorMask.export();
4paths.forEach(path => {
5  console.log(path);  // 路径节点数据
6});复制代码


path是一个对象,有几个字段比较关键:

字段 说明
recordType 节点类型
numPoints 闭合节点的数量
preceding 起点控制点
anchor 路径节点坐标点
leaving 终点控制点


recordType


recordType记录着节点的类型,关于类型的说明能够参照这里,搜"path records",有几个须要关注的类型:

recordType 说明
0 起始点
1 闭合的贝塞尔曲线点
2 闭合的路径点,precedingleaving能够忽略
4 非闭合的贝塞尔曲线的
5 非闭合的路径点,precedingleaving能够忽略


numPoints


标记连续路径的节点数量,须要经过这个字段判断路径的结束位置。


preceding/anchor/leaving


preceding、anchor、leaving记录着路径节点中,三个控制点相对于PSD画布的位置信息。每一个字段对应的控制点以下图:



转换路径信息


preceding、anchor、leaving这三个控制点的数据类型对象,包含两个字段horizvert,对应x和y坐标点的位置。但这里有个地方须要留意的,一般咱们会用像素距离来描述某个点的位置,例以下图的点a:



点a相对画布的位置为x:10,y:60。


可是,PSD文档中的路径节点的控制点的坐标数据是两个无符号的浮点数,是相对于画布左上角原点的像素距离与画布宽高的比例,例以下图的点a:



点a相对画布的位置也能够描述为x:0.05,y:0.3。


为了更好的将PSD路径数据导出到SVG中,咱们须要对这些控制点的位置进行一个转换,将比例位置转化成像素位置,同时须要将无符号浮点数转化成符号浮点数。


1// 转化无符号浮点数
 2const signed = function(n) {
 3  let num = n;
 4  if (num > 0x8f) {
 5    num = num - 0xff - 1;
 6  }
 7
 8  return num;
 9};
10
11const getPathPosition = function(pathNode) {
12  const {
13    vert,
14    horiz
15  } = pathNode;
16
17  return {
18    x: signed(horiz),
19    y: signed(vert)
20  };
21}
22
23const parsePath = function(path, { width, height }) {
24  const {
25    preceding,
26    anchor,
27    leaving
28  } = path;
29
30  const precedingPos = this.getPathPosition(preceding);
31  const anchorPos = this.getPathPosition(anchor);
32  const leavingPos = this.getPathPosition(leaving);
33  
34  // relX 和 relY 保留了PSD中原始数据。
35  return {
36    preceding: {
37      relX: precedingPos.x,
38      relY: precedingPos.y,
39      x: Math.round(width * precedingPos.x),
40      y: Math.round(height * precedingPos.y)
41    },
42    anchor: {
43      relX: anchorPos.x,
44      relY: anchorPos.y,
45      x: Math.round(width * anchorPos.x),
46      y: Math.round(height * anchorPos.y)
47    },
48    leaving: {
49      relX: leavingPos.x,
50      relY: leavingPos.y,
51      x: Math.round(width * leavingPos.x),
52      y: Math.round(height * leavingPos.y)
53    }
54  };
55}
56
57const vectorMask = node.get('vectorMask');
58vectorMask.parse();
59const paths = vectorMask.export();
60const convertedPath = []
61paths.forEach(path => {
62  // 转换控制点的位置
63  // 这里的 document 为psd.js导出的psd文档对象
64  const { recordType, numPoints } = path;
65  const {
66    preceding,
67    anchor,
68    leaving
69  } = parsePath(path, document);  // 控制点的位置转换成了像素位置
70  
71  convertedPath.push({
72    preceding,
73    anchor,
74    leaving
75  });
76});复制代码


转换成SVG的path标签


按照path标签d属性的语法


1const toPath = (paths) => {
 2  let head;
 3  const data = [];
 4  
 5  paths.forEach((path, index) => {
 6    const { preceding, anchor, leaving } = path;
 7    if (index < paths.length - 1) {
 8      if (index > 0) {  // 中间节点
 9        data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y}`);
10      } else {  // 记录第一个节点,用于在关闭路径的时候使用
11        head = path;
12        data.push(`M ${anchor.x}, ${anchor.y} C${leaving.x}, ${leaving.y}`);
13      }
14    } else {
15      data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y} ${head.preceding.x}, ${head.preceding.y} ${head.anchor.x}, ${head.anchor.y} Z`);
16    }
17  });
18  
19  return `<path d="${data.join(' ')}" />`;
20}复制代码


给图形填充颜色


若是图形填充的是纯色,能够经过以下方式获取。


1const getFillColor = function(node) {
 2  const solidColorData = node.get('solidColor');
 3  const clr = solidColorData['Clr '];
 4
 5  return toHexColor([
 6    Math.round(clr['Rd ']),
 7    Math.round(clr['Grn ']),
 8    Math.round(clr['Bl '])
 9  ]);
10};复制代码


对以前的toPath方法进行一下改造。


1const toPath = (paths, fill) => {
 2  let head;
 3  const data = [];
 4  
 5  paths.forEach((path, index) => {
 6    const { preceding, anchor, leaving } = path;
 7    if (index < paths.length - 1) {
 8      if (index > 0) {  // 中间节点
 9        data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y}`);
10      } else {  // 记录第一个节点,用于在关闭路径的时候使用
11        head = path;
12        data.push(`M ${anchor.x}, ${anchor.y} C${leaving.x}, ${leaving.y}`);
13      }
14    } else {
15      data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y} ${head.preceding.x}, ${head.preceding.y} ${head.anchor.x}, ${head.anchor.y} Z`);
16    }
17  });
18  
19  return `<path d="${data.join(' ')}" fill="${fill}" />`;
20}复制代码


范例


用PS制做一个只有图形的PSD文档



导出后的svg文档:


1<?xml version="1.0" encoding="UTF-8"?>
 2<!-- generated by lst-postman -->
 3<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 4  viewBox="0 0 750 300"
 5  enable-background="new 0 0 750 300"
 6  xml:space="preserve"
 7>
 8  
 9  <image x="0" y="0" width="750" height="300" overflow="visible" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu4AAAEsCAYAAACc1TboAAAAAklEQVR4AewaftIAAAWHSURBVO3BMQHAMAADoBxxMcX1WC+djRxAz3dfAACAaQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvB/WUAcwL7APngAAAABJRU5ErkJggg=="></image>
10  <path d="M 246, 98 C351, 135 401, 55 422, 86 443, 117 464, 167 533, 125 602, 83 699, 115 636, 174 573, 233 408, 272 328, 245 248, 218 252, 171 144, 204 93, 220 122, 54 246, 98 Z" fill="#00ff15"></path>
11</svg>复制代码


如有收获,就赏束稻谷吧

相关文章
相关标签/搜索