记 psd.js 解析

1,149 阅读5分钟

前言

记录 psd.js 使用过程中遇到的各种解析问题,版本基于 3.9.0

psd 解析

一、文件导入

import PSD from "psd.js"
// 本地文件导入
PSD.fromDroppedFile(file)
// Load from URL
const url = URL.createObjectURL(file);
PSD.fromURL(url)
// Load from event, e.g. drag & drop
function onDrop(evt) {
  PSD.fromEvent(evt)
}

二、文件导入后获取 psd 对象

PSD.fromDroppedFile(file).then((psd) => { 
    console.log(psd)
    // 下面取 psd 值都是由这里取值
});

1. psd 对象中的属性

Snipaste_2024-05-13_11-58-39.png

header
psd: {
    header: {
        height, // 高度
        width, // 宽度
        mode, // 颜色模式 数字几代表数组中第几位,例如 mode:3 为RGB颜色模式 ["Bitmap", "GrayScale", "IndexedColor", "RGBColor", "CMYKColor", "HSLColor", "HSBColor", "Multichannel", "Duotone", "LabColor", "Gray16", "RGB48", "Lab48", "CMYK64", "DeepMultichannel", "Duotone16"]
        depth, // 位深度
        channels, // 颜色通道数量
        sig, // sig应始终等于'8BPS',如果签名与此值不匹配,请不要尝试读取文件。
        version // version赢总是等于1,如果版本与此值不匹配,请不要尝试读取该文件。
    }
}
image
// 获取预览图 
psd.image.toBase64()
// 输出 png
psd.image.toPng() // <img width="xxx" height="xxx" src="data:image/png;base64,xxxx" >
resources
psd.resources.resources[1037].angle // 全局光角度
psd.resources.resources[1005].v_res // 分辨率
psd.resources.resources[1005].v_res_unit // 分辨率模式 1 像素/英寸 2 像素/厘米
parsed

parsed 代表是否已解析

2. 获取图层

psd.tree().children()

三、图层属性

1. 获取所有图层

PSD.fromDroppedFile(file).then((psd) => { 
    const children = psd.tree().children() 
    children.forEach((item, index) => {
        if(item.type == "layer") // 单个图层
        if(item.type == "group") {
            // 组
            // 递归去遍历下级每一个图层信息
        } 
    }
});

2. 通用属性

{
    top,
    bottom,
    left,
    right,
    width,
    height,
    name, // 图层名称
    type, // 图层类型
    opacity, // 透明度
    visible, // 显示隐藏
}

判断图层是否锁定

export const getLocked = (item) => {
  const locked = item.layer.locked()
  return locked?.allLocked || locked?.obj?.positionLocked
}

3. 判断当前图层是否图片或者文字等

这里的方法是判断当前图层是文字、形状、图片或者路径

export const getLayerType = (item) => {
  let layerType = "image" // 默认类型为图片
  if (item.get("typeTool")) layerType = "text" // 文字
  else if (item.get("vectorOrigination")) {
    if (item.get("vectorMask")) layerType = "path" // 优先判断为路径
    const vectorOrigination = item.get("vectorOrigination")
    const { keyOriginType, keyOriginShapeBBox } = vectorOrigination.data.keyDescriptorList[0]
    if (keyOriginType == 4) layerType = "line" // 线条
    else if (keyOriginType == 5) {
      const { Btom, Left, Rght } = keyOriginShapeBBox
      const Top = keyOriginShapeBBox["Top "]
      if (Btom.value - Top.value === Rght.value - Left.value) {
        layerType = "circle" // 圆形
      } else {
        layerType = "ellipse" // 椭圆
      }
    }
    if ([1, 2].includes(keyOriginType)) {
      layerType = "rect" // 矩形
    }
  }
  return layerType
}

四、文本属性

以下是整理的文字独有属性

1. 文字整体属性

// 获取字体
const resolveFontFamily = function (typeTool, index) {
    const fontFamily = typeTool.engineData.ResourceDict.FontSet[index]
    return fontFamily?.Name
}

// 这里是因为 psd.js 自带的 color 数组遇到默认颜色是会不插入数组(字号同理),导致数组长度与需要遍历的数组长度对不上,所以这里我们单独处理下颜色
const getNewFontsStyles = (typeTool, psd) => {
    if (typeTool.engineData == null) {
      return {}
    }
    const data = typeTool.engineData.EngineDict.StyleRun.RunArray.map(function (r) {
      // 缺少默认颜色时添加默认颜色
      if (!r.StyleSheet.StyleSheetData.FillColor) {
        r.StyleSheet.StyleSheetData.FillColor = {
          Type: 1,
          Values: [1, 0, 0, 0]
        }
      }
      // 缺少字号时添加默认字号
      if (!r.StyleSheet.StyleSheetData.FontSize) {
        r.StyleSheet.StyleSheetData.FontSize = (psd?.resources?.resources[1005]?.v_res / 72) * 12
      }
      return r.StyleSheet.StyleSheetData
    })
    return (typeTool._styles = data.reduce(function (m, o) {
      for (const k in o) {
        if (!o.hasOwnProperty(k)) {
          continue
        }
        const v = o[k]
        m[k] || (m[k] = [])
        m[k].push(v)
      }
      return m
    }, {}))
}

const getNewFontsColors = (typeTool, psd) => {
    if (typeTool.engineData == null || getNewFontsStyles(typeTool, psd).FillColor == null) {
      return [[0, 0, 0, 255]]
    }
    return getNewFontsStyles(typeTool, psd).FillColor.map(function (s) {
      const values = s.Values.map(function (v) {
        return Math.round(v * 255)
      })
      values.push(values.shift())
      return values
    })
}

const getNewFontsSizes = (typeTool, psd) => {
    if (typeTool.engineData == null && getNewFontsStyles(typeTool, psd).FontSize == null) {
      return []
    }
    return getNewFontsStyles(typeTool, psd).FontSize.map(function (s) {
      return s
    })
}

export const getTextObj = (item, psd) => {
    const layer = item.layer
    const typeTool = item.get("typeTool")
    const { textData } = typeTool
    const { text } = item.export()
    const { transform, font } = text
    const { xx, xy, tx, yx, yy, ty } = transform
    const StyleSheet = layer.adjustments.typeTool.obj.engineData.EngineDict.StyleRun.RunArray[0].StyleSheet || {}
    const { StyleSheetData } = StyleSheet
    const { weights, alignment, textDecoration, styles, leading } = font
    const colors = getNewFontsColors(typeTool, psd)
    const sizes = getNewFontsSizes(typeTool, psd)
    const vertical = textData.Ornt.value == "Vrtc"
    const fontFamily = resolveFontFamily(typeTool, StyleSheetData.Font) || font.names[0]
    
    return {
        fontFamily, // 字体
        weights: weights && weights[0], // 字体粗细
        sizes: sizes && Math.round(sizes[0] * yy * 100) * 0.01, // 字体大小
        colors: colors && colors[0], // 字体颜色
        alignment: alignment && alignment[0], // 水平对齐方式
        textDecoration: (textDecoration && textDecoration[0]) == "underline", // 是否有下划线
        styles: styles && styles[0], // 字体风格
        leading: leading && sizes && typeof leading[0] == "number" ? leading[0] / sizes[0] : 1, // 字体行高
        vertical, // 判断横竖排文字
        linethrough: StyleSheetData.Strikethrough, // 删除线
        charSpacing: StyleSheetData.Tracking, // 字间距 
        angle: Math.round(Math.atan(transform.xy / transform.xx) * (180 / Math.PI)), // 旋转角度
        flipX: xx < 0, // 是否横向翻转
        flipY: yy < 0, // 是否垂直翻转
    }
}

2. 单个文字属性

psd 可以给单个图层文字中部分文字赋值样式,这里是获取部分文字单独样式

Snipaste_2024-05-13_17-59-25.png

// 获取字体
const resolveFontFamily = function (typeTool, index) {
    const fontFamily = typeTool.engineData.ResourceDict.FontSet[index]
    return fontFamily?.Name
}

// 这里是因为 psd.js 自带的 color 数组遇到默认颜色是会不插入数组(字号同理),导致数组长度与需要遍历的数组长度对不上,所以这里我们单独处理下颜色
const getNewFontsStyles = (typeTool, psd) => {
    if (typeTool.engineData == null) {
      return {}
    }
    const data = typeTool.engineData.EngineDict.StyleRun.RunArray.map(function (r) {
      // 缺少默认颜色时添加默认颜色
      if (!r.StyleSheet.StyleSheetData.FillColor) {
        r.StyleSheet.StyleSheetData.FillColor = {
          Type: 1,
          Values: [1, 0, 0, 0]
        }
      }
      // 缺少字号时添加默认字号
      if (!r.StyleSheet.StyleSheetData.FontSize) {
        r.StyleSheet.StyleSheetData.FontSize = (psd?.resources?.resources[1005]?.v_res / 72) * 12
      }
      return r.StyleSheet.StyleSheetData
    })
    return (typeTool._styles = data.reduce(function (m, o) {
      for (const k in o) {
        if (!o.hasOwnProperty(k)) {
          continue
        }
        const v = o[k]
        m[k] || (m[k] = [])
        m[k].push(v)
      }
      return m
    }, {}))
}

const getNewFontsColors = (typeTool, psd) => {
    if (typeTool.engineData == null || getNewFontsStyles(typeTool, psd).FillColor == null) {
      return [[0, 0, 0, 255]]
    }
    return getNewFontsStyles(typeTool, psd).FillColor.map(function (s) {
      const values = s.Values.map(function (v) {
        return Math.round(v * 255)
      })
      values.push(values.shift())
      return values
    })
}

const getNewFontsSizes = (typeTool, psd) => {
    if (typeTool.engineData == null && getNewFontsStyles(typeTool, psd).FontSize == null) {
      return []
    }
    return getNewFontsStyles(typeTool, psd).FontSize.map(function (s) {
      return s
    })
}

// 获取单个字体颜色方法
export const getTextObj = (item, psd) => {
    const layer = item.layer
    const typeTool = item.get("typeTool")
    const { text } = item.export()
    const { transform, font } = text
    const { xx, xy, tx, yx, yy, ty } = transform
    const { weights, textDecoration, styles, leading } = font
    const colors = getNewFontsColors(typeTool, psd)
    const sizes = getNewFontsSizes(typeTool, psd)
    const obj = {}
    if (font.lengthArray.length > 1) {
        const textArr = textObj.value.split("\r")
        textArr.forEach((item, index) => {
            obj[index] = {}
        })
        let num = 0 // 总字数
        let textArrIndex = 0 // 行数
        let objIndex = 0 // 行内字数位置
        font.lengthArray.forEach((item, index) => {
          const itemStyleSheet = layer.adjustments.typeTool.obj.engineData.EngineDict.StyleRun.RunArray[index].StyleSheet || {}
          const { StyleSheetData } = itemStyleSheet
          for (let i = num; i < num + item; i++) {
          if (num > textArr[textArrIndex].length && textArrIndex < textArr.length - 1) {
            textArrIndex++
            objIndex = 0
          }
          const itemfontFamily = resolveFontFamily(typeTool, StyleSheetData.Font) || font.names[0]
          if (!isSupportFontFamily(itemfontFamily)) fontFamilyLisr.push(itemfontFamily)
          obj[textArrIndex][objIndex] = {
            fontFamily: itemfontFamily,
            weights: (weights && weights[index]) || "normal", // 字体粗细
            sizes:  Math.round(sizes[index] * yy * 100) * 0.01, // 字体大小
            colors: Array.isArray(colors && colors[index]) ? `rgba(${colors[index].join(",")})` : colors ? colors[0] : "", // 字体颜色
            textDecoration: (textDecoration && textDecoration[index]) == "underline", // 是否有下划线
            styles: (styles && styles[index]) || "normal", // 字体风格
            leading: leading && sizes && typeof leading[index] == "number" ? leading[index] / sizes[index] : 1, // 字体行高
            linethrough: StyleSheetData.Strikethrough || false, // 删除线
            charSpacing: StyleSheetData.Tracking || 0 // 字间距
          }
          objIndex++
        }
        num = num + item
        })
    }
    return obj
}

五、混合选项

1. 投影 DrSh

const getShadow = (psd, item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { DrSh = {} } = objectEffects.data
    if (DrSh?.enab) {
      const clrStr = JSON.stringify(DrSh["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item2.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      // 是否使用了全局光
      const angle = DrSh.uglg ? psd?.resources?.resources[1037]?.angle : DrSh.lagl.value
      const distance = DrSh.Dstn.value
      const color = `rgba(${parseInt(r)},${parseInt(g)},${parseInt(b)},${DrSh.Opct.value / 100})`
      let theta = (angle * Math.PI) / 180
      let offsetX = Number((-distance * Math.cos(theta)).toFixed(2))
      let offsetY = Number((distance * Math.sin(theta)).toFixed(2))
      return {
        angle, // 投影角度
        color, // 投影颜色
        blur: DrSh.blur.value, // 投影大小
        distance, // 投影距离
        offsetX,
        offsetY,
      }
    } else return {}
}

2. 描边 FrFX

const getStrokeAttrs = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { FrFX = {} } = objectEffects.data || {}
    if (FrFX?.enab) {
      const clrStr = JSON.stringify(FrFX["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item2.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item2.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      const strokeOpacity = FrFX.Opct.value / 100
      const strokeColor = `rgba(${r}, ${g}, ${b}, ${strokeOpacity})`
      const strokeSize = FrFX["Sz  "].value
      const strokePosition = FrFX.Styl.value
      return {
        stroke: strokeColor, // 描边颜色
        strokeWidth: strokeSize, // 描边大小
        strokePosition // 描边类型 OutF 外部 | InsF 内部 | CtrF 居中
      }
    } else return {}
}

3. 渐变叠加 GrFl

const getEffectsGradient = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { GrFl = {} } = objectEffects.data || {}
    if (GrFl?.enab) {
        const { Grad, Type, Angl } = GrFl || {}
        const { Clrs, Trns } = Grad || {}
        const colorStops = Clrs.map((color, index) => {
          return {
            color: `rgba(${parseInt(color["Clr "]["Rd  "])},${parseInt(color["Clr "]["Grn "])},${parseInt(
              color["Clr "]["Bl  "]
            )},${Trns[index] ? Trns[index].Opct.value / 100 : 1})`,
            offset: Number((color.Lctn / 4096 / 1.2).toFixed(2))
          }
        })
        const angle = Angl.value
        return {
          type: Type.value, // 渐变样式 Lnr 线性 | Rdl 径向 | Angl 角度 | Rflc 对称的 | Dmnd 菱形
          color: colorStops, // 渐变色区间
          angle // 渐变角度
        }
    } else return {}
}

4. 颜色叠加 SoFi

const getEffectsColor = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { SoFi = {} } = objectEffects.data || {}
    if (SoFi?.enab) {
      const clrStr = JSON.stringify(SoFi["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      const color = `rgba(${parseInt(r)},${parseInt(g)},${parseInt(b)},${SoFi.Opct.value / 100})`
      return {
        color
      }
    } else return {}
}

5. 光泽 ChFX

const getGloss = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { ChFX = {} } = objectEffects.data
    if (ChFX?.enab) {
      const {blur, Dstn, lagl, Opct} = ChFX || {}
      const clrStr = JSON.stringify(ChFX["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item2.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      const distance = Dstn.value
      const color = `rgba(${parseInt(r)},${parseInt(g)},${parseInt(b)},${Opct.value / 100})`
      const angle = lagl.value
      const blur = blur.value
      let theta = (angle * Math.PI) / 180
      let offsetX = Number((-distance * Math.cos(theta)).toFixed(2))
      let offsetY = Number((distance * Math.sin(theta)).toFixed(2))
      return {
        angle, // 光泽角度
        color, // 光泽颜色
        blur, // 光泽大小
        distance, // 光泽距离
        offsetX,
        offsetY,
      }
    } else return {}
}

5. 外发光 OrGl

const getGlow = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { OrGl = {} } = objectEffects.data
    if (OrGl?.enab) {
      const {blur, Ckmt, Inpr, Nose, Opct, ShdN} = OrGl || {}
      const clrStr = JSON.stringify(OrGl["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item2.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      const color = `rgba(${parseInt(r)},${parseInt(g)},${parseInt(b)},${Opct.value / 100})`
      return {
        color, // 颜色
        extend: Ckmt.value, // 扩展
        blur: blur.value, // 大小
        variegated: Nose.value, // 杂色
        range: Inpr.value, // 范围
        shake: ShdN.value, // 抖动
      }
    } else return {}
}

6. 内发光 IrGl

const getInnerGlow = (item) => {
    const objectEffects = item.layer.objectEffects && item.layer.objectEffects()
    const { IrGl = {} } = objectEffects.data
    if (IrGl?.enab) {
      const {blur, Ckmt, Inpr, Nose, Opct, ShdN} = IrGl || {}
      const clrStr = JSON.stringify(IrGl["Clr "]).split(",")
      let r = null
      let g = null
      let b = null
      clrStr.forEach((item2) => {
        if (item2.indexOf("Rd") !== -1) {
          r = item2.replace('"Rd  ":', "")
        } else if (item2.indexOf("Bl") !== -1) {
          b = item2.replace('"Bl  ":', "").replace("}", "")
        } else if (item.indexOf("Grn") !== -1) {
          g = item2.replace('"Grn ":', "")
        }
      })
      const color = `rgba(${parseInt(r)},${parseInt(g)},${parseInt(b)},${Opct.value / 100})`
      return {
        color, // 颜色
        extend: Ckmt.value, // 扩展
        blur: blur.value, // 大小
        variegated: Nose.value, // 杂色
        range: Inpr.value, // 范围
        shake: ShdN.value, // 抖动
        glwS: glwS.value // 源 SrcC 柔和 | glwS 边缘
      }
    } else return {}
}

六、调整图层

1. 亮度对比度 brightnessContrast

const getBrightnessContrast = (item) => {
    const brightnessContrast = item.get("brightnessContrast")
    if (brightnessContrast) { 
        brightnessContrast.file.seek(brightnessContrast.startPos)
        const brightness = brightnessContrast.file.readShort()
        const contrast = brightnessContrast.file.readShort()
        return {
            brightness, // 亮度
            contrast // 对比度
        }
    } else return {}
}

结尾

目前先整理一部分,陆续整理中...
欢迎大伙指出问题