使用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>複製代碼


如有收穫,就賞束稻穀吧

相關文章
相關標籤/搜索