轉載至微信公共號: 方凳雅集javascript
隨着發展,活動會場頁面的題圖運營須要線上模板化,而自研的導購素材製做平臺接入了海棠-創意中心,經過平臺能力,將素材模板化,而且經過配置化的方式生成多種場景化,個性化的素材。可是創意中心的素材模板是基於SVG的,而會場頁面的題圖基本是基於Photoshop(PS)輸出,源文件是PSD。因爲SVG是面向矢量圖形的標記語言,而PS是以位圖處理爲中心的圖像處理軟件,大多時候,PS沒法直接導出SVG文件。html
爲了能讓會場頁面的題圖模板接入到導購素材製做平臺,同時下降設計師的使用門檻,咱們須要在導購素材製做平臺中實現直接將PSD轉成SVG的功能,在線化的將PSD轉成SVG,而後導入到創意中心,將題圖模板化。前端
PSD文檔是一個二進制文件,官網有相關的格式說明文檔。目前,在前端解析PSD文檔比較成熟的方案是使用 psd.js ,使用方法和API能夠參見這裏。java
能夠經過以下的方式導出PSD文檔的JSON結構。node
const PSD = require('psd');
const psd = PSD.fromFile(file); // file爲psd文檔的存儲路徑
psd.parse();
console.log(psd.tree().export());
複製代碼
JSON結構大概以下:git
{ 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
下的圖層信息。github
psd.js也支持導出單個圖層的JSON。json
// 訪問圖層節點
const node = psd.tree().childrenAtPath('Version A/Matte')[0];
// or
const node =psd.tree().childrenAtPath(['Version A', 'Matte'])[0];
// 獲取圖層信息
node.export();
複製代碼
圖層信息都記錄在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
,記錄文檔的編碼、命名空間、視口(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編碼的內容以字符串的形式嵌到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'));
// ...
});
複製代碼
關鍵步驟:
pack
方法將圖片數據轉成stream對象data
事件,獲取流數據Buffer
將流數據轉換成Base64字符串內聯的方式有個好處,就是圖片的數據能夠打包到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'));
// ...
});
複製代碼
關鍵步驟在於:
另一個會常常遇到的場景,就是處理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;
}
複製代碼
另一個地方,PSD文檔中文本圖層記錄的字體族,並不是咱們平時看到的Font Family,而是PostScript Name,關於如何獲取字體信息,推薦一個網站,opentype.js,上傳字體後,能夠看到字體的詳細信息,以及符號集。
而opentype.js也是一個可靠強大的字體包解析工具庫,親測好使,牆裂推薦。
轉載至 微信公衆號:方凳雅集
關於文字圖層的定位,SVG的text
標籤提供兩種方式。
x
和y
屬性transform
屬性而psd.js導出的PSD數據中,能夠經過相應的字段,計算獲得。
經(you)過(shi)實(cai)踐(keng),我推薦使用transform
進行文字定位。實現以下:
const resolveTransformAttrValue = function(text) {
const { transform } = text;
return `matrix(1 0 0 1 ${transform.tx} ${transform.ty})`;
}
複製代碼
爲啥?由於阿里媽媽的海棠-創意中心的SVG模板支持transform屬性定位文字。