我正在尝试将 PSD 路径记录转换为 SVG 路径数据。
因为我不想交叉发帖,所以我会链接到原始问题。想要复制相关资料的朋友可以在这里复制回来
基本上,我得到 PSD,解析它,并从矢量蒙版对象中获取形状信息。
它包含一个名为
paths
的数组,看起来像下面显示的点:
_ = require 'lodash'
# A path record describes a single point in a vector path. This is used
# in a couple of different places, but most notably in vector shapes.
module.exports = class PathRecord
constructor: (@file) ->
@recordType = null
parse: ->
@recordType = @file.readShort()
switch @recordType
when 0, 3 then @_readPathRecord()
when 1, 2, 4, 5 then @_readBezierPoint()
when 7 then @_readClipboardRecord()
when 8 then @_readInitialFill()
else @file.seek(24, true)
export: ->
_.merge { recordType: @recordType }, switch @recordType
when 0, 3 then { numPoints: @numPoints }
when 1, 2, 4, 5
linked: @linked
closed: (@recordType in [1, 2])
preceding:
vert: @precedingVert
horiz: @precedingHoriz
anchor:
vert: @anchorVert
horiz: @anchorHoriz
leaving:
vert: @leavingVert
horiz: @leavingHoriz
when 7
clipboard:
top: @clipboardTop
left: @clipboardLeft
bottom: @clipboardBottom
right: @clipboardRight
resolution: @clipboardResolution
when 8 then { initialFill: @initialFill }
else {}
isBezierPoint: -> @recordType in [1, 2, 4, 5]
_readPathRecord: ->
@numPoints = @file.readShort()
@file.seek 22, true
_readBezierPoint: ->
@linked = @recordType in [1, 4]
@precedingVert = @file.readPathNumber()
@precedingHoriz = @file.readPathNumber()
@anchorVert = @file.readPathNumber()
@anchorHoriz = @file.readPathNumber()
@leavingVert = @file.readPathNumber()
@leavingHoriz = @file.readPathNumber()
_readClipboardRecord: ->
@clipboardTop = @file.readPathNumber()
@clipboardLeft = @file.readPathNumber()
@clipboardBottom = @file.readPathNumber()
@clipboardRight = @file.readPathNumber()
@clipboardResolution = @file.readPathNumber()
@file.seek 4, true
_readInitialFill: ->
@initialFill = @file.readShort()
@file.seek 22, true
我正在尝试将该信息转换为 SVG 路径数据,但我被困在两点上。什么记录与什么路径命令相关,数据似乎是小于 1 的值。
这是您可以在 Photoshop 中创建的老虎形状的示例路径数据:
我整理了数据
[
{
"recordType": 6
},
{
"recordType": 8,
"initialFill": 0
},
{
"recordType": 0,
"numPoints": 257
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.14081686735153198,
"horiz": 0.07748442888259888
},
"anchor": {
"vert": 0.14081686735153198,
"horiz": 0.0777387022972107
},
"leaving": {
"vert": 0.13936221599578857,
"horiz": 0.0777667760848999
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.13929903507232666,
"horiz": 0.07793217897415161
},
"anchor": {
"vert": 0.1385088562965393,
"horiz": 0.07837295532226562
},
"leaving": {
"vert": 0.13777965307235718,
"horiz": 0.07837295532226562
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.13706856966018677,
"horiz": 0.07837295532226562
},
"anchor": {
"vert": 0.13632577657699585,
"horiz": 0.07837295532226562
},
"leaving": {
"vert": 0.1364198923110962,
"horiz": 0.07855236530303955
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.13649815320968628,
"horiz": 0.07873183488845825
},
"anchor": {
"vert": 0.13657790422439575,
"horiz": 0.07890427112579346
},
"leaving": {
"vert": 0.1359773874282837,
"horiz": 0.07879406213760376
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.13536030054092407,
"horiz": 0.07869088649749756
},
"anchor": {
"vert": 0.1347590684890747,
"horiz": 0.07858771085739136
},
"leaving": {
"vert": 0.13486969470977783,
"horiz": 0.07879406213760376
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.13499760627746582,
"horiz": 0.07900881767272949
},
"anchor": {
"vert": 0.13512402772903442,
"horiz": 0.07922220230102539
},
"leaving": {
"vert": 0.1344437599182129,
"horiz": 0.07920092344284058
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.1268816590309143,
"horiz": 0.08006417751312256
},
"anchor": {
"vert": 0.12613815069198608,
"horiz": 0.08038073778152466
},
"leaving": {
"vert": 0.12613815069198608,
"horiz": 0.08055287599563599
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.12613815069198608,
"horiz": 0.08073228597640991
},
"anchor": {
"vert": 0.12613815069198608,
"horiz": 0.08091175556182861
},
"leaving": {
"vert": 0.1256791353225708,
"horiz": 0.0807945728302002
}
},
{
"recordType": 2,
"linked": false,
"closed": true,
"preceding": {
"vert": 0.12177199125289917,
"horiz": 0.08080857992172241
},
"anchor": {
"vert": 0.12177199125289917,
"horiz": 0.08080857992172241
},
"leaving": {
"vert": 0.12169301509857178,
"horiz": 0.08107715845108032
}
}
]
github上的帖子有解析数据的功能
photoshop 路径模型基于 绘图点 定义路径 – 每个点都可以包含 2 个“切线手柄”。
这个模型实际上反映了UI概念您将在图形应用程序中体验:您可以从任何绘图点拉出切线手柄来控制路径段的曲率。
point.anchor
– 可视化设置绘图点point.preceding
– 左切线句柄point.leaving
– 右切线句柄
与 psd 模型相比,SVG
C
命令将从前一个点的 leaving
坐标开始,如下所示:
previousPoint.leaving
– 前一点的切线句柄point.preceding
– 右切线句柄point.anchor
– 终点/位置
正确的
point.leaving
已经属于svg中的下一个命令
所以你需要重新排序检索到的点/坐标数据.
recordType
到 svg 指令类型您可以忽略它们——这些属性仅与 photoshop UI 行为相关。
幸运的是,您可以轻松地将所有 psd 点转换为
C
cubic curvetos。唯一的例外是强制性的M
(moveto)命令,它被描述为:
firstPoint.anchor
L
(linetos) 也由 3 个对象属性描述:如果 preceding
、anchor
和 leaving
相等——我们可以将这些点转换为 L
命令(具有相等值的 C
命令2 个控制点和最后一个点也可以。
如前所述,我们需要重新排序/移动 photoshop 点数据:
最后一个关闭命令需要第一个点的
preceding
坐标:
point.leaving
pointStart.preceding
– 第一点左切线句柄pointStart.anchor
– 第一点/位置
实际上有点奇怪——坐标是相对于 psd 文档的宽度和高度存储的。
scaleX = psdDocumentWidth
scaleY = psdDocumentHeight
因为我们不必为像
V
或 H
或 A
(弧)这样的速记命令而烦恼,我们可以在一个循环中乘以 x(偶数 - 从 0 开始)和 y(奇数)值。
let clip = clipPath;
// remove first 3 entries from array
clip.splice(0, 3);
/**
* clip path might be a compound path containing sub paths
*/
let subPaths = splitSubpaths(clip);
// collect path data
let pathData = [];
subPaths.forEach((subPath) => {
pathData.push({
type: "M",
values: [subPath[0].anchor.horiz, subPath[0].anchor.vert]
});
for (let i = 1; i < subPath.length; i++) {
let com = subPath[i];
let prev = subPath[i - 1] ? subPath[i - 1] : subPath[i];
let p0 = { x: prev.anchor.horiz, y: prev.anchor.vert };
let cp1 = { x: prev.leaving.horiz, y: prev.leaving.vert };
let cp2 = { x: com.preceding.horiz, y: com.preceding.vert };
let p = { x: com.anchor.horiz, y: com.anchor.vert };
/**
* is lineto -
* if preceding, anchor and leaving points
* are equal
*/
if (
com.preceding.horiz === com.leaving.horiz &&
com.preceding.vert === com.leaving.vert &&
com.anchor.horiz === com.preceding.horiz &&
com.anchor.vert === com.preceding.vert &&
com.anchor.horiz === com.leaving.horiz &&
com.anchor.vert === com.leaving.vert
) {
pathData.push({
type: "L",
values: [p.x, p.y]
});
}
// is cubic
else {
pathData.push({
type: "C",
values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y]
});
}
}
// close path
let pointL = subPath.length - 1;
let comLast = {};
let isClosingLineTo = false;
if (
subPath[pointL].leaving.horiz === subPath[pointL].preceding.horiz &&
subPath[pointL].leaving.vert === subPath[pointL].preceding.vert &&
subPath[pointL].anchor.horiz === subPath[pointL].preceding.horiz &&
subPath[pointL].anchor.vert === subPath[pointL].preceding.vert
) {
isClosingLineTo = true;
}
if(!isClosingLineTo){
pathData.push({
type: "C",
values: [
subPath[pointL].leaving.horiz,
subPath[pointL].leaving.vert,
subPath[0].preceding.horiz,
subPath[0].preceding.vert,
subPath[0].anchor.horiz,
subPath[0].anchor.vert
]
});
}
// all clip paths are closed – append z command
pathData.push({
type: "z",
values: []
});
});
//scale according to psd doc width
pathData = scalePathData(pathData, docWidth, docHeight, 3);
// convert path data to d attribute
let d = pathDataToD(pathData);
path.setAttribute("d", d);
svg.setAttribute("viewBox", [0, 0, docWidth, docHeight].join(" "));
// show output
svgOut.value = new XMLSerializer().serializeToString(svg);
function scalePathData(pathData, scaleX = 1, scaleY = 1, decimals = 3) {
let pathDataScaled = [];
pathData.forEach((com, i) => {
if (pathData[i].values.length) {
for (let v = 0; v < com.values.length; v++) {
let scale = v % 2 === 0 ? scaleX : scaleY;
pathData[i].values[v] = +(com.values[v] * scale).toFixed(decimals);
}
}
});
return pathData;
}
/**
* pathData to svg d attribute
*/
function pathDataToD(pathData) {
let d = pathData
.map((com) => {
return `${com.type}${com.values.join(" ")}`;
})
.join("");
// optimize whitespace and delimiters
d = d.replaceAll(",", " ").replaceAll(" -", "-");
return d;
}
/**
* split compound paths into sub path array chunks
*/
function splitSubpaths(pathPointArray) {
let subPathArr = [];
let subPathMindex = [0];
pathPointArray.forEach((com, i) => {
// starting new subpath
if (!com.anchor) {
subPathMindex.push(i);
pathPointArray.splice(i, 1);
}
});
// create subpath array
subPathMindex.forEach((index, i) => {
let n = subPathMindex[i + 1];
let thisSeg = pathPointArray.slice(index, n);
subPathArr.push(thisSeg);
});
return subPathArr;
}
svg{
height: 20em;
border: 1px solid #ccc;
}
textarea{
display:block;
width:100%;
min-height:20em;
}
<svg id="svg" viewBox="0 0 100 100">
<path id="path" d="" />
</svg>
<h3>Output</h3>
<textarea id="svgOut" ></textarea>
<script>
let docWidth = 100;
let docHeight = 50;
let clipPath = [
// first 3 entries can be removed
{ recordType: 6 },
{ recordType: 8, initialFill: 0 },
{ recordType: 0, numPoints: 4 },
// first point anchor = M command
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.20999997854232788, horiz: 0.14666664600372314 },
anchor: { vert: 0.20999997854232788, horiz: 0.2149999737739563 },
leaving: { vert: 0.20999997854232788, horiz: 0.32833331823349 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.3233333230018616, horiz: 0.3799999952316284 },
anchor: { vert: 0.4699999690055847, horiz: 0.3799999952316284 },
leaving: { vert: 0.6166666150093079, horiz: 0.3799999952316284 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.6399999856948853, horiz: 0.38499999046325684 },
anchor: { vert: 0.6399999856948853, horiz: 0.29499995708465576 },
leaving: { vert: 0.6399999856948853, horiz: 0.20499998331069946 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.7666666507720947, horiz: 0.11499994993209839 },
anchor: { vert: 0.6399999856948853, horiz: 0.11499994993209839 },
leaving: { vert: 0.5133333206176758, horiz: 0.11499994993209839 }
},
// init new sub path - will be omitted
{ recordType: 0, numPoints: 4 },
// start sub path data
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.3799999952316284, horiz: 0.3009999990463257 },
anchor: { vert: 0.3699999451637268, horiz: 0.2799999713897705 },
leaving: { vert: 0.35192763805389404, horiz: 0.24204808473587036 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.3779999613761902, horiz: 0.22299998998641968 },
anchor: { vert: 0.4299999475479126, horiz: 0.22499996423721313 },
leaving: { vert: 0.4819999933242798, horiz: 0.22699999809265137 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5239999890327454, horiz: 0.23499995470046997 },
anchor: { vert: 0.5299999713897705, horiz: 0.26999998092651367 },
leaving: { vert: 0.5359999537467957, horiz: 0.3049999475479126 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5339999794960022, horiz: 0.32799994945526123 },
anchor: { vert: 0.4899999499320984, horiz: 0.32499998807907104 },
leaving: { vert: 0.44599997997283936, horiz: 0.3219999670982361 }
},
// new sub path
{ recordType: 0, numPoints: 4 },
// preceding == anchor == leaving => L lineto command
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5299999713897705, horiz: 0.18499994277954102 },
anchor: { vert: 0.5299999713897705, horiz: 0.18499994277954102 },
leaving: { vert: 0.5299999713897705, horiz: 0.18499994277954102 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5299999713897705, horiz: 0.1499999761581421 },
anchor: { vert: 0.5299999713897705, horiz: 0.1499999761581421 },
leaving: { vert: 0.5299999713897705, horiz: 0.1499999761581421 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5799999833106995, horiz: 0.1499999761581421 },
anchor: { vert: 0.5799999833106995, horiz: 0.1499999761581421 },
leaving: { vert: 0.5799999833106995, horiz: 0.1499999761581421 }
},
{
recordType: 1,
linked: true,
closed: true,
preceding: { vert: 0.5799999833106995, horiz: 0.1799999475479126 },
anchor: { vert: 0.5799999833106995, horiz: 0.1799999475479126 },
leaving: { vert: 0.5799999833106995, horiz: 0.1799999475479126 }
}
];
</script>