如何将 PSD 矢量路径转换为 SVG 路径数据

问题描述 投票:0回答:1

我正在尝试将 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上的帖子有解析数据的功能

https://github.com/meltingice/psd.js/issues/284

javascript html svg psd psdjs
1个回答
0
投票

不同的路径对象模型

Photoshop路径模型:基于绘图点

photoshop 路径模型基于 绘图点 定义路径 – 每个点都可以包含 2 个“切线手柄”

这个模型实际上反映了UI概念您将在图形应用程序中体验:您可以从任何绘图点拉出切线手柄来控制路径段的曲率。

point.anchor
– 可视化设置绘图点
point.preceding
– 左切线句柄
point.leaving
– 右切线句柄

SVG 路径模型:基于段

与 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(奇数)值。

示例:将 psd 剪辑路径转换为 svg

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>

© www.soinside.com 2019 - 2024. All rights reserved.