前言
记录 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 对象中的属性
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 可以给单个图层文字中部分文字赋值样式,这里是获取部分文字单独样式
// 获取字体
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 {}
}
结尾
目前先整理一部分,陆续整理中...
欢迎大伙指出问题