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

背景

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

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

相关

  • 基础篇(文字&图片)

处理图形

在PS中,绘制图形一般会用到钢笔工具。

29b01290aab79e832548979486c511c9.png

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

8914ae1b5b6e5fcf53c03357508cef3f.png

SVG的全称又叫Scalable Vector Graphics,本身就是面向矢量图形的标记语言,所以,对于PSD中的图形路径的信息,理论上是可以映射到SVG中的。

在SVG中,用于显示图形的标签有很多:

  • rect
  • ellipse
  • circle
  • line
  • polyline
  • polygon
  • path

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

我们能不能将不同图形的绘制都统一成一种解法呢?

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

获取路径节点

使用psd.js可以通过如下方式获取到图形路径的信息。

const vectorMask = node.get('vectorMask');
vectorMask.parse();
const paths = vectorMask.export();
paths.forEach(path => {
  console.log(path);  // 路径节点数据
});

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画布的位置信息。每个字段对应的控制点如下图:

b1b8b4a1a4ccc1c63c425422368799df.png

转换路径信息

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

65ef6782ad354981289a1a7aa4c5f2df.png

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

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

6b5b29642d051a5a03837fbd4dd21e20.png

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

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

// 转化无符号浮点数
const signed = function(n) {
  let num = n;
  if (num > 0x8f) {
    num = num - 0xff - 1;
  }

  return num;
};

const getPathPosition = function(pathNode) {
  const {
    vert,
    horiz
  } = pathNode;

  return {
    x: signed(horiz),
    y: signed(vert)
  };
}

const parsePath = function(path, { width, height }) {
  const {
    preceding,
    anchor,
    leaving
  } = path;

  const precedingPos = this.getPathPosition(preceding);
  const anchorPos = this.getPathPosition(anchor);
  const leavingPos = this.getPathPosition(leaving);

  // relX 和 relY 保留了PSD中原始数据。
  return {
    preceding: {
      relX: precedingPos.x,
      relY: precedingPos.y,
      x: Math.round(width * precedingPos.x),
      y: Math.round(height * precedingPos.y)
    },
    anchor: {
      relX: anchorPos.x,
      relY: anchorPos.y,
      x: Math.round(width * anchorPos.x),
      y: Math.round(height * anchorPos.y)
    },
    leaving: {
      relX: leavingPos.x,
      relY: leavingPos.y,
      x: Math.round(width * leavingPos.x),
      y: Math.round(height * leavingPos.y)
    }
  };
}

const vectorMask = node.get('vectorMask');
vectorMask.parse();
const paths = vectorMask.export();
const convertedPath = []
paths.forEach(path => {
  // 转换控制点的位置
  // 这里的 document 为psd.js导出的psd文档对象
  const { recordType, numPoints } = path;
  const {
    preceding,
    anchor,
    leaving
  } = parsePath(path, document);  // 控制点的位置转换成了像素位置

  convertedPath.push({
    preceding,
    anchor,
    leaving
  });
});

转换成SVG的path标签

按照path标签d属性的语法

const toPath = (paths) => {
  let head;
  const data = [];

  paths.forEach((path, index) => {
    const { preceding, anchor, leaving } = path;
    if (index < paths.length - 1) {
      if (index > 0) {  // 中间节点
        data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y}`);
      } else {  // 记录第一个节点,用于在关闭路径的时候使用
        head = path;
        data.push(`M ${anchor.x}, ${anchor.y} C${leaving.x}, ${leaving.y}`);
      }
    } else {
      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`);
    }
  });

  return `<path d="${data.join(' ')}" />`;
}

给图形填充颜色

如果图形填充的是纯色,可以通过如下方式获取。

const getFillColor = function(node) {
  const solidColorData = node.get('solidColor');
  const clr = solidColorData['Clr '];

  return toHexColor([
    Math.round(clr['Rd  ']),
    Math.round(clr['Grn ']),
    Math.round(clr['Bl  '])
  ]);
};

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

const toPath = (paths, fill) => {
  let head;
  const data = [];

  paths.forEach((path, index) => {
    const { preceding, anchor, leaving } = path;
    if (index < paths.length - 1) {
      if (index > 0) {  // 中间节点
        data.push(`${preceding.x}, ${preceding.y} ${anchor.x}, ${anchor.y} ${leaving.x}, ${leaving.y}`);
      } else {  // 记录第一个节点,用于在关闭路径的时候使用
        head = path;
        data.push(`M ${anchor.x}, ${anchor.y} C${leaving.x}, ${leaving.y}`);
      }
    } else {
      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`);
    }
  });

  return `<path d="${data.join(' ')}" fill="${fill}" />`;
}

范例

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

e891b9536c4a8d40cbfaa1cec84635af.png

导出后的svg文档:

<?xml version="1.0" encoding="UTF-8"?>
<!-- generated by lst-postman -->
<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 750 300"
  enable-background="new 0 0 750 300"
  xml:space="preserve"
>

  <image x="0" y="0" width="750" height="300" overflow="visible" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu4AAAEsCAYAAACc1TboAAAAAklEQVR4AewaftIAAAWHSURBVO3BMQHAMAADoBxxMcX1WC+djRxAz3dfAACAaQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvAYAAJjXAAAA8xoAAGBeAwAAzGsAAIB5DQAAMK8BAADmNQAAwLwGAACY1wAAAPMaAABgXgMAAMxrAACAeQ0AADCvAQAA5jUAAMC8BgAAmNcAAADzGgAAYF4DAADMawAAgHkNAAAwrwEAAOY1AADAvB/WUAcwL7APngAAAABJRU5ErkJggg=="></image>
  <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>
</svg>

256c2f2c542b45ce9ff6e555237727e2.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ERROR Failed to compile with 48 errors 上午10:53:54 These dependencies were not found: * core-js/modules/es.array.push.js in ./node_modules/.store/@babel+runtime@7.22.6/node_modules/@babel/runtime/helpers/esm/objectSpread2.js, ./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/.store/babel-loader@8.3.0/node_modules/babel-loader/lib!./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/.store/vue-loader@15.10.1/node_modules/vue-loader/lib??vue-loader-options!./src/components/HeaderSearch/index.vue?vue&type=script&lang=js& and 29 others * core-js/modules/es.error.cause.js in ./node_modules/.store/@babel+runtime@7.22.6/node_modules/@babel/runtime/helpers/esm/regeneratorRuntime.js, ./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/.store/babel-loader@8.3.0/node_modules/babel-loader/lib!./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/.store/vue-loader@15.10.1/node_modules/vue-loader/lib??vue-loader-options!./src/layout/components/Navbar.vue?vue&type=script&lang=js& and 5 others * core-js/modules/es.object.proto.js in ./node_modules/.store/@babel+runtime@7.22.6/node_modules/@babel/runtime/helpers/esm/regeneratorRuntime.js * core-js/modules/es.regexp.dot-all.js in ./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/.store/babel-loader@8.3.0/node_modules/babel-loader/lib!./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/.store/vue-loader@15.10.1/node_modules/vue-loader/lib??vue-loader-options!./src/components/ThemePicker/index.vue?vue&type=script&lang=js&, ./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/.store/babel-loader@8.3.0/node_modules/babel-loader/lib!./node_modules/.store/cache-loader@4.1.0/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/.store/vue-loader@15.10.1/node_modules/vue-loader/lib??vue-loader-options!./src/layout/components/Navbar.vue?vue&type=script&lang=js& and 2 others * core-js/modules/web.url-search-params.delete.js in ./src/utils/request.js * core-js/modules/web.url-search-params.has.js in ./src/utils/request.js * core-js/modules/web.url-search-params.size.js in ./src/utils/request.js * qs in ./src/utils/request.js * svg-baker-runtime/browser-symbol in ./src/icons/svg/user.svg To install them, you can run: npm install --save core-js/modules/es.array.push.js core-js/modules/es.error.cause.js core-js/modules/es.object.proto.js core-js/modules/es.regexp.dot-all.js core-js/modules/web.url-search-params.delete.js core-js/modules/web.url-search-params.has.js core-js/modules/web.url-search-params.size.js qs svg-baker-runtime/browser-symbol怎么解决如何安装
07-21

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值