ag-psd
Version:
Library for reading and writing PSD files
1,430 lines (1,308 loc) • 74.8 kB
text/typescript
import { createEnum } from './helpers';
import {
AntiAlias, BevelDirection, BevelStyle, BevelTechnique, BlendMode, Color, EffectContour,
EffectNoiseGradient, EffectPattern, EffectSolidGradient, ExtraGradientInfo, ExtraPatternInfo,
GlowSource, GlowTechnique, GradientStyle, InterpolationMethod, LayerEffectBevel,
LayerEffectGradientOverlay, LayerEffectInnerGlow, LayerEffectPatternOverlay,
LayerEffectSatin, LayerEffectShadow, LayerEffectsInfo, LayerEffectSolidFill,
LayerEffectsOuterGlow, LayerEffectStroke, LineAlignment, LineCapType, LineJoinType,
Orientation, TextGridding, TimelineKey, TimelineKeyInterpolation, TimelineTrack, TimelineTrackType,
Units, UnitsBounds, UnitsValue, VectorContent, WarpStyle
} from './psd';
import {
PsdReader, readSignature, readUnicodeString, readUint32, readUint8, readFloat64,
readBytes, readAsciiString, readInt32, readFloat32, readInt32LE, readUnicodeStringWithLengthLE
} from './psdReader';
import {
PsdWriter, writeSignature, writeBytes, writeUint32, writeFloat64, writeUint8,
writeUnicodeStringWithPadding, writeInt32, writeFloat32, writeUnicodeString, writeInt32LE,
writeUnicodeStringWithoutLengthLE
} from './psdWriter';
interface Dict { [key: string]: string; }
interface NameClassID { name: string; classID: string; }
interface ExtTypeDict { [key: string]: NameClassID; }
function revMap(map: Dict) {
const result: Dict = {};
Object.keys(map).forEach(key => result[map[key]] = key);
return result;
}
const unitsMap: Dict = {
'#Ang': 'Angle',
'#Rsl': 'Density',
'#Rlt': 'Distance',
'#Nne': 'None',
'#Prc': 'Percent',
'#Pxl': 'Pixels',
'#Mlm': 'Millimeters',
'#Pnt': 'Points',
'RrPi': 'Picas',
'RrIn': 'Inches',
'RrCm': 'Centimeters',
};
const unitsMapRev = revMap(unitsMap);
let logErrors = false;
export function setLogErrors(value: boolean) {
logErrors = value;
}
function makeType(name: string, classID: string) {
return { name, classID };
}
const nullType = makeType('', 'null');
const USE_CHINESE = false; // Testing
const fieldToExtType: ExtTypeDict = {
strokeStyleContent: makeType('', 'solidColorLayer'),
printProofSetup: makeType(USE_CHINESE ? '校样设置' : 'Proof Setup', 'proofSetup'),
Grad: makeType(USE_CHINESE ? '渐变' : 'Gradient', 'Grdn'),
Trnf: makeType(USE_CHINESE ? '变换' : 'Transform', 'Trnf'),
patternFill: makeType('', 'patternFill'),
ebbl: makeType('', 'ebbl'),
SoFi: makeType('', 'SoFi'),
GrFl: makeType('', 'GrFl'),
sdwC: makeType('', 'RGBC'),
hglC: makeType('', 'RGBC'),
'Clr ': makeType('', 'RGBC'),
'tintColor': makeType('', 'RGBC'),
Ofst: makeType('', 'Pnt '),
ChFX: makeType('', 'ChFX'),
MpgS: makeType('', 'ShpC'),
DrSh: makeType('', 'DrSh'),
IrSh: makeType('', 'IrSh'),
OrGl: makeType('', 'OrGl'),
IrGl: makeType('', 'IrGl'),
TrnS: makeType('', 'ShpC'),
Ptrn: makeType('', 'Ptrn'),
FrFX: makeType('', 'FrFX'),
phase: makeType('', 'Pnt '),
frameStep: nullType,
duration: nullType,
workInTime: nullType,
workOutTime: nullType,
audioClipGroupList: nullType,
bounds: makeType('', 'Rctn'),
customEnvelopeWarp: makeType('', 'customEnvelopeWarp'),
warp: makeType('', 'warp'),
'Sz ': makeType('', 'Pnt '),
origin: makeType('', 'Pnt '),
autoExpandOffset: makeType('', 'Pnt '),
keyOriginShapeBBox: makeType('', 'unitRect'),
Vrsn: nullType,
psVersion: nullType,
docDefaultNewArtboardBackgroundColor: makeType('', 'RGBC'),
artboardRect: makeType('', 'classFloatRect'),
keyOriginRRectRadii: makeType('', 'radii'),
keyOriginBoxCorners: nullType,
rectangleCornerA: makeType('', 'Pnt '),
rectangleCornerB: makeType('', 'Pnt '),
rectangleCornerC: makeType('', 'Pnt '),
rectangleCornerD: makeType('', 'Pnt '),
compInfo: nullType,
quiltWarp: makeType('', 'quiltWarp'),
generatorSettings: nullType,
crema: nullType,
FrIn: nullType,
blendOptions: nullType,
FXRf: nullType,
Lefx: nullType,
time: nullType,
animKey: nullType,
timeScope: nullType,
inTime: nullType,
outTime: nullType,
sheetStyle: nullType,
translation: nullType,
Skew: nullType,
boundingBox: makeType('', 'boundingBox'),
'Lnk ': makeType('', 'ExternalFileLink'),
frameReader: makeType('', 'FrameReader'),
effectParams: makeType('', 'motionTrackEffectParams'),
Impr: makeType('None', 'none'),
Anch: makeType('', 'Pnt '),
'Fwd ': makeType('', 'Pnt '),
'Bwd ': makeType('', 'Pnt '),
FlrC: makeType('', 'Pnt '),
meshBoundaryPath: makeType('', 'pathClass'),
filterFX: makeType('', 'filterFXStyle'),
Fltr: makeType('', 'rigidTransform'),
FrgC: makeType('', 'RGBC'),
BckC: makeType('', 'RGBC'),
sdwM: makeType('Parameters', 'adaptCorrectTones'),
hglM: makeType('Parameters', 'adaptCorrectTones'),
customShape: makeType('', 'customShape'),
origFXRefPoint: nullType,
FXRefPoint: nullType,
ClMg: makeType('', 'ClMg'),
};
const fieldToArrayExtType: ExtTypeDict = {
'Crv ': makeType('', 'CrPt'),
Clrs: makeType('', 'Clrt'),
Trns: makeType('', 'TrnS'),
keyDescriptorList: nullType,
solidFillMulti: makeType('', 'SoFi'),
gradientFillMulti: makeType('', 'GrFl'),
dropShadowMulti: makeType('', 'DrSh'),
innerShadowMulti: makeType('', 'IrSh'),
frameFXMulti: makeType('', 'FrFX'),
FrIn: nullType,
FSts: nullType,
LaSt: nullType,
sheetTimelineOptions: nullType,
trackList: makeType('', 'animationTrack'),
globalTrackList: makeType('', 'animationTrack'),
keyList: nullType,
audioClipGroupList: nullType,
audioClipList: nullType,
countObjectList: makeType('', 'countObject'),
countGroupList: makeType('', 'countGroup'),
slices: makeType('', 'slice'),
'Pts ': makeType('', 'Pthp'),
SbpL: makeType('', 'SbpL'),
pathComponents: makeType('', 'PaCm'),
filterFXList: makeType('', 'filterFX'),
puppetShapeList: makeType('', 'puppetShape'),
channelDenoise: makeType('', 'channelDenoiseParams'),
ShrP: makeType('', 'Pnt '),
layerSettings: nullType,
list: nullType,
Adjs: makeType('', 'CrvA'),
};
const typeToField: { [key: string]: string[]; } = {
'TEXT': [
'Txt ', 'printerName', 'Nm ', 'Idnt', 'blackAndWhitePresetFileName', 'LUT3DFileName',
'presetFileName', 'curvesPresetFileName', 'mixerPresetFileName', 'placed', 'description', 'reason',
'artboardPresetName', 'json', 'clipID', 'relPath', 'fullPath', 'mediaDescriptor', 'Msge',
'altTag', 'url', 'cellText', 'preset', 'KnNm', 'FPth', 'comment', 'originalPath',
],
'tdta': [
'EngineData', 'LUT3DFileData', 'indexArray', 'originalVertexArray', 'deformedVertexArray',
'LqMe',
],
'long': [
'TextIndex', 'RndS', 'Mdpn', 'Smth', 'Lctn', 'strokeStyleVersion', 'LaID', 'Vrsn', 'Cnt ',
'Brgh', 'Cntr', 'means', 'vibrance', 'Strt', 'bwPresetKind', 'comp', 'compID', 'originalCompID',
'curvesPresetKind', 'mixerPresetKind', 'uOrder', 'vOrder', 'PgNm', 'totalPages', 'Crop',
'numerator', 'denominator', 'frameCount', 'Annt', 'keyOriginType', 'unitValueQuadVersion',
'keyOriginIndex', 'major', 'minor', 'fix', 'docDefaultNewArtboardBackgroundType', 'artboardBackgroundType',
'numModifyingFX', 'deformNumRows', 'deformNumCols', 'FrID', 'FrDl', 'FsID', 'LCnt', 'AFrm', 'AFSt',
'numBefore', 'numAfter', 'Spcn', 'minOpacity', 'maxOpacity', 'BlnM', 'sheetID', 'gblA', 'globalAltitude',
'descVersion', 'frameReaderType', 'LyrI', 'zoomOrigin', 'fontSize', 'Rds ', 'sliceID',
'topOutset', 'leftOutset', 'bottomOutset', 'rightOutset', 'filterID', 'meshQuality',
'meshExpansion', 'meshRigidity', 'VrsM', 'VrsN', 'NmbG', 'WLMn', 'WLMx', 'AmMn', 'AmMx', 'SclH', 'SclV',
'Lvl ', 'TlNm', 'TlOf', 'FlRs', 'Thsh', 'ShrS', 'ShrE', 'FlRs', 'Vrnc', 'Strg', 'ExtS', 'ExtD',
'HrzS', 'VrtS', 'NmbR', 'EdgF', 'Ang1', 'Ang2', 'Ang3', 'Ang4', 'lastAppliedComp', 'capturedInfo',
],
'enum': [
'textGridding', 'Ornt', 'warpStyle', 'warpRotate', 'Inte', 'Bltn', 'ClrS', 'BlrQ',
'bvlT', 'bvlS', 'bvlD', 'Md ', 'glwS', 'GrdF', 'GlwT', 'RplS', 'BlrM', 'SmBM',
'strokeStyleLineCapType', 'strokeStyleLineJoinType', 'strokeStyleLineAlignment',
'strokeStyleBlendMode', 'PntT', 'Styl', 'lookupType', 'LUTFormat', 'dataOrder',
'tableOrder', 'enableCompCore', 'enableCompCoreGPU', 'compCoreSupport', 'compCoreGPUSupport', 'Engn',
'enableCompCoreThreads', 'gs99', 'FrDs', 'trackID', 'animInterpStyle', 'horzAlign',
'vertAlign', 'bgColorType', 'shapeOperation', 'UndA', 'Wvtp', 'Drct', 'WndM', 'Edg ', 'FlCl', 'IntE',
'IntC', 'Cnvr', 'Fl ', 'Dstr', 'MztT', 'Lns ', 'ExtT', 'DspM', 'ExtR', 'ZZTy', 'SphM', 'SmBQ', 'placedLayerOCIOConversion', 'gradientsInterpolationMethod',
],
'bool': [
'PstS', 'printSixteenBit', 'masterFXSwitch', 'enab', 'uglg', 'antialiasGloss',
'useShape', 'useTexture', 'uglg', 'antialiasGloss', 'useShape', 'Vsbl',
'useTexture', 'Algn', 'Rvrs', 'Dthr', 'Invr', 'VctC', 'ShTr', 'layerConceals',
'strokeEnabled', 'fillEnabled', 'strokeStyleScaleLock', 'strokeStyleStrokeAdjust',
'hardProof', 'MpBl', 'paperWhite', 'useLegacy', 'Auto', 'Lab ', 'useTint', 'keyShapeInvalidated',
'autoExpandEnabled', 'autoNestEnabled', 'autoPositionEnabled', 'shrinkwrapOnSaveEnabled',
'present', 'showInDialog', 'overprint', 'sheetDisclosed', 'lightsDisclosed', 'meshesDisclosed',
'materialsDisclosed', 'hasMotion', 'muted', 'Effc', 'selected', 'autoScope', 'fillCanvas',
'cellTextIsHTML', 'Smoo', 'Clsp', 'validAtPosition', 'rigidType', 'hasoptions', 'filterMaskEnable',
'filterMaskLinked', 'filterMaskExtendWithWhite', 'removeJPEGArtifact', 'Mnch', 'ExtF', 'ExtM',
'moreAccurate', 'GpuY', 'LIWy', 'Cnty',
],
'doub': [
'warpValue', 'warpPerspective', 'warpPerspectiveOther', 'Intr', 'Wdth', 'Hght',
'strokeStyleMiterLimit', 'strokeStyleResolution', 'layerTime', 'keyOriginResolution',
'xx', 'xy', 'yx', 'yy', 'tx', 'ty', 'FrGA', 'frameRate', 'audioLevel', 'rotation',
'X ', 'Y ', 'redFloat', 'greenFloat', 'blueFloat', 'imageResolution',
'PuX0', 'PuX1', 'PuX2', 'PuX3', 'PuY0', 'PuY1', 'PuY2', 'PuY3'
],
'UntF': [
'sdwO', 'hglO', 'lagl', 'Lald', 'srgR', 'blur', 'Sftn', 'Opct', 'Dstn', 'Angl',
'Ckmt', 'Nose', 'Inpr', 'ShdN', 'strokeStyleLineWidth', 'strokeStyleLineDashOffset',
'strokeStyleOpacity', 'H ', 'Top ', 'Left', 'Btom', 'Rght', 'Rslt',
'topRight', 'topLeft', 'bottomLeft', 'bottomRight', 'ClNs', 'Shrp',
],
'VlLs': [
'Crv ', 'Clrs', 'Mnm ', 'Mxm ', 'Trns', 'pathList', 'strokeStyleLineDashSet', 'FrLs', 'slices',
'LaSt', 'Trnf', 'nonAffineTransform', 'keyDescriptorList', 'guideIndeces', 'gradientFillMulti',
'solidFillMulti', 'frameFXMulti', 'innerShadowMulti', 'dropShadowMulti', 'FrIn', 'FSts', 'FsFr',
'sheetTimelineOptions', 'audioClipList', 'trackList', 'globalTrackList', 'keyList', 'audioClipList',
'warpValues', 'selectedPin', 'Pts ', 'SbpL', 'pathComponents', 'pinOffsets', 'posFinalPins',
'pinVertexIndices', 'PinP', 'PnRt', 'PnOv', 'PnDp', 'filterFXList', 'puppetShapeList', 'ShrP',
'channelDenoise', 'Mtrx', 'layerSettings', 'list', 'compList', 'Adjs',
],
'ObAr': ['meshPoints', 'quiltSliceX', 'quiltSliceY'],
'obj ': ['null', 'Chnl'],
'Pth ': ['DspF'],
};
const channels = [
'Rd ', 'Grn ', 'Bl ', 'Yllw', 'Ylw ', 'Cyn ', 'Mgnt', 'Blck', 'Gry ', 'Lmnc', 'A ', 'B ',
];
const fieldToArrayType: Dict = {
'Mnm ': 'long',
'Mxm ': 'long',
FrLs: 'long',
strokeStyleLineDashSet: 'UntF',
Trnf: 'doub',
nonAffineTransform: 'doub',
keyDescriptorList: 'Objc',
gradientFillMulti: 'Objc',
solidFillMulti: 'Objc',
frameFXMulti: 'Objc',
innerShadowMulti: 'Objc',
dropShadowMulti: 'Objc',
LaSt: 'Objc',
FrIn: 'Objc',
FSts: 'Objc',
FsFr: 'long',
blendOptions: 'Objc',
sheetTimelineOptions: 'Objc',
keyList: 'Objc',
warpValues: 'doub',
selectedPin: 'long',
'Pts ': 'Objc',
SbpL: 'Objc',
pathComponents: 'Objc',
pinOffsets: 'doub',
posFinalPins: 'doub',
pinVertexIndices: 'long',
PinP: 'doub',
PnRt: 'long',
PnOv: 'bool',
PnDp: 'doub',
filterFXList: 'Objc',
puppetShapeList: 'Objc',
ShrP: 'Objc',
channelDenoise: 'Objc',
Mtrx: 'long',
compList: 'long',
Chnl: 'enum',
};
const fieldToType: Dict = {};
for (const type of Object.keys(typeToField)) {
for (const field of typeToField[type]) {
fieldToType[field] = type;
}
}
for (const field of Object.keys(fieldToExtType)) {
if (!fieldToType[field]) fieldToType[field] = 'Objc';
}
for (const field of Object.keys(fieldToArrayExtType)) {
fieldToArrayType[field] = 'Objc';
}
function getTypeByKey(key: string, value: any, root: string, parent: any) {
if (key === 'presetKind') {
return typeof value === 'string' ? 'enum' : 'long';
} if (key === 'null' && root === 'slices') {
return 'TEXT';
} else if (key === 'groupID') {
return root === 'slices' ? 'long' : 'TEXT';
} else if (key === 'Sz ') {
return ('Wdth' in value) ? 'Objc' : (('units' in value) ? 'UntF' : 'doub');
} else if (key === 'Type') {
return typeof value === 'string' ? 'enum' : 'long';
} else if (key === 'AntA') {
return typeof value === 'string' ? 'enum' : 'bool';
} else if ((key === 'Hrzn' || key === 'Vrtc') && (parent.Type === 'keyType.Pstn' || parent._classID === 'Ofst')) {
return 'long';
} else if (key === 'Hrzn' || key === 'Vrtc' || key === 'Top ' || key === 'Left' || key === 'Btom' || key === 'Rght') {
if (root === 'slices') return 'long';
return typeof value === 'number' ? 'doub' : 'UntF';
} else if (key === 'Vrsn') {
return typeof value === 'number' ? 'long' : 'Objc';
} else if (key === 'Rd ' || key === 'Grn ' || key === 'Bl ') {
return root === 'artd' ? 'long' : 'doub';
} else if (key === 'Trnf') {
return Array.isArray(value) ? 'VlLs' : 'Objc';
} else {
return fieldToType[key];
}
}
export function readAsciiStringOrClassId(reader: PsdReader) {
const length = readInt32(reader);
return readAsciiString(reader, length || 4);
}
function writeAsciiStringOrClassId(writer: PsdWriter, value: string) {
if (value.length === 4 && value !== 'warp' && value !== 'time' && value !== 'hold' && value !== 'list') {
// write classId
writeInt32(writer, 0);
writeSignature(writer, value);
} else {
// write ascii string
writeInt32(writer, value.length);
for (let i = 0; i < value.length; i++) {
writeUint8(writer, value.charCodeAt(i));
}
}
}
export function readDescriptorStructure(reader: PsdReader, includeClass: boolean) {
const struct = readClassStructure(reader);
const object: any = includeClass ? { _name: struct.name, _classID: struct.classID } : {};
// console.log('>> ', struct);
const itemsCount = readUint32(reader);
for (let i = 0; i < itemsCount; i++) {
const key = readAsciiStringOrClassId(reader);
const type = readSignature(reader);
// console.log(`> '${key}' '${type}'`);
const data = readOSType(reader, type, includeClass);
// if (!getTypeByKey(key, data)) console.log(`> '${key}' '${type}'`, data);
object[key] = data;
}
return object;
}
export function writeDescriptorStructure(writer: PsdWriter, name: string, classId: string, value: any, root: string) {
if (logErrors && !classId) console.log('Missing classId for: ', name, classId, value);
// write class structure
writeUnicodeStringWithPadding(writer, name);
writeAsciiStringOrClassId(writer, classId);
const keys = Object.keys(value);
let keyCount = keys.length;
if ('_name' in value) keyCount--;
if ('_classID' in value) keyCount--;
writeUint32(writer, keyCount);
for (const key of keys) {
if (key === '_name' || key === '_classID') continue;
let type = getTypeByKey(key, value[key], root, value);
let extType = fieldToExtType[key];
if (key === 'bounds' && root === 'text') {
extType = makeType('', 'bounds');
} else if (key === 'origin') {
type = root === 'slices' ? 'enum' : 'Objc';
} else if ((key === 'Cyn ' || key === 'Mgnt' || key === 'Ylw ' || key === 'Blck') && value._classID === 'CMYC') {
type = 'doub';
} else if (/^PN[a-z][a-z]$/.test(key)) {
type = 'TEXT';
} else if (/^PT[a-z][a-z]$/.test(key)) {
type = 'long';
} else if (/^PF[a-z][a-z]$/.test(key)) {
type = 'doub';
} else if ((key === 'Rds ' || key === 'Thsh') && typeof value[key] === 'number' && value._classID === 'SmrB') {
type = 'doub';
} else if (key === 'ClSz' || key === 'Rds ' || key === 'Amnt') {
type = typeof value[key] === 'number' ? 'long' : 'UntF';
} else if ((key === 'sdwM' || key === 'hglM') && typeof value[key] === 'string') {
type = 'enum';
} else if (key === 'blur' && typeof value[key] === 'string') {
type = 'enum';
} else if (key === 'Hght' && typeof value[key] === 'number' && value._classID === 'Embs') {
type = 'long';
} else if (key === 'Angl' && typeof value[key] === 'number' && (value._classID === 'Embs' || value._classID === 'smartSharpen' || value._classID === 'Twrl' || value._classID === 'MtnB')) {
type = 'long';
} else if (key === 'Angl' && typeof value[key] === 'number') {
type = 'doub'; // ???
} else if (key === 'bounds' && root === 'slices') {
type = 'Objc';
extType = makeType('', 'Rct1');
} else if (key === 'Scl ') {
if (typeof value[key] === 'object' && 'Hrzn' in value[key]) {
type = 'Objc';
extType = nullType;
} else if (typeof value[key] === 'number') {
type = 'long';
} else {
type = 'UntF';
}
} else if (key === 'audioClipGroupList' && keys.length === 1) {
type = 'VlLs';
} else if ((key === 'Strt' || key === 'Brgh') && 'H ' in value) {
type = 'doub';
} else if (key === 'Wdth' && typeof value[key] === 'object') {
type = 'UntF';
} else if (key === 'Ofst' && typeof value[key] === 'number') {
type = 'long';
} else if (key === 'Strt' && typeof value[key] === 'object') {
type = 'Objc';
extType = nullType;
} else if (channels.indexOf(key) !== -1) {
type = (classId === 'RGBC' && root !== 'artd') ? 'doub' : 'long';
} else if (key === 'profile') {
type = classId === 'printOutput' ? 'TEXT' : 'tdta';
} else if (key === 'strokeStyleContent') {
if (value[key]['Clr ']) {
extType = makeType('', 'solidColorLayer');
} else if (value[key].Grad) {
extType = makeType('', 'gradientLayer');
} else if (value[key].Ptrn) {
extType = makeType('', 'patternLayer');
} else {
logErrors && console.log('Invalid strokeStyleContent value', value[key]);
}
} else if (key === 'bounds' && root === 'quiltWarp') {
extType = makeType('', 'classFloatRect');
}
if (extType && extType.classID === 'RGBC') {
if ('H ' in value[key]) extType = { classID: 'HSBC', name: '' };
// TODO: other color spaces
}
writeAsciiStringOrClassId(writer, key);
writeSignature(writer, type || 'long');
writeOSType(writer, type || 'long', value[key], key, extType, root);
if (logErrors && !type) console.log(`Missing descriptor field type for: '${key}' in`, value);
}
}
function readOSType(reader: PsdReader, type: string, includeClass: boolean) {
switch (type) {
case 'obj ': // Reference
return readReferenceStructure(reader);
case 'Objc': // Descriptor
case 'GlbO': // GlobalObject same as Descriptor
return readDescriptorStructure(reader, includeClass);
case 'VlLs': { // List
const length = readInt32(reader);
const items: any[] = [];
for (let i = 0; i < length; i++) {
const itemType = readSignature(reader);
// console.log(' >', itemType);
items.push(readOSType(reader, itemType, includeClass));
}
return items;
}
case 'doub': // Double
return readFloat64(reader);
case 'UntF': { // Unit double
const units = readSignature(reader);
const value = readFloat64(reader);
if (!unitsMap[units]) throw new Error(`Invalid units: ${units}`);
return { units: unitsMap[units], value };
}
case 'UnFl': { // Unit float
const units = readSignature(reader);
const value = readFloat32(reader);
if (!unitsMap[units]) throw new Error(`Invalid units: ${units}`);
return { units: unitsMap[units], value };
}
case 'TEXT': // String
return readUnicodeString(reader);
case 'enum': { // Enumerated
const enumType = readAsciiStringOrClassId(reader);
const value = readAsciiStringOrClassId(reader);
return `${enumType}.${value}`;
}
case 'long': // Integer
return readInt32(reader);
case 'comp': { // Large Integer
const low = readUint32(reader);
const high = readUint32(reader);
return { low, high };
}
case 'bool': // Boolean
return !!readUint8(reader);
case 'type': // Class
case 'GlbC': // Class
return readClassStructure(reader);
case 'alis': { // Alias
const length = readInt32(reader);
return readAsciiString(reader, length);
}
case 'tdta': { // Raw Data
const length = readInt32(reader);
return readBytes(reader, length);
}
case 'ObAr': { // Object array
readInt32(reader); // version: 16
readUnicodeString(reader); // name: ''
readAsciiStringOrClassId(reader); // 'rationalPoint'
const length = readInt32(reader);
const items: any[] = [];
for (let i = 0; i < length; i++) {
const type1 = readAsciiStringOrClassId(reader); // type Hrzn | Vrtc
readSignature(reader); // UnFl
readSignature(reader); // units ? '#Pxl'
const valuesCount = readInt32(reader);
const values: number[] = [];
for (let j = 0; j < valuesCount; j++) {
values.push(readFloat64(reader));
}
items.push({ type: type1, values });
}
return items;
}
case 'Pth ': { // File path
/*const length =*/ readInt32(reader); // total size of all fields below
const sig = readSignature(reader);
/*const pathSize =*/ readInt32LE(reader); // the same as length
const charsCount = readInt32LE(reader);
const path = readUnicodeStringWithLengthLE(reader, charsCount);
return { sig, path };
}
default:
throw new Error(`Invalid TySh descriptor OSType: ${type} at ${reader.offset.toString(16)}`);
}
}
const ObArTypes: { [key: string]: string | undefined; } = {
meshPoints: 'rationalPoint',
quiltSliceX: 'UntF',
quiltSliceY: 'UntF',
};
function writeOSType(writer: PsdWriter, type: string, value: any, key: string, extType: NameClassID | undefined, root: string) {
switch (type) {
case 'obj ': // Reference
writeReferenceStructure(writer, key, value);
break;
case 'Objc': // Descriptor
case 'GlbO': { // GlobalObject same as Descriptor
if (typeof value !== 'object') throw new Error(`Invalid struct value: ${JSON.stringify(value)}, key: ${key}`);
if (!extType) throw new Error(`Missing ext type for: '${key}' (${JSON.stringify(value)})`);
const name = value._name || extType.name;
const classID = value._classID || extType.classID;
writeDescriptorStructure(writer, name, classID, value, root);
break;
}
case 'VlLs': // List
if (!Array.isArray(value)) throw new Error(`Invalid list value: ${JSON.stringify(value)}, key: ${key}`);
writeInt32(writer, value.length);
for (let i = 0; i < value.length; i++) {
const type = fieldToArrayType[key];
writeSignature(writer, type || 'long');
writeOSType(writer, type || 'long', value[i], `${key}[]`, fieldToArrayExtType[key], root);
if (logErrors && !type) console.log(`Missing descriptor array type for: '${key}' in`, value);
}
break;
case 'doub': // Double
if (typeof value !== 'number') throw new Error(`Invalid number value: ${JSON.stringify(value)}, key: ${key}`);
writeFloat64(writer, value);
break;
case 'UntF': // Unit double
if (!unitsMapRev[value.units]) throw new Error(`Invalid units: ${value.units} in ${key}`);
writeSignature(writer, unitsMapRev[value.units]);
writeFloat64(writer, value.value);
break;
case 'UnFl': // Unit float
if (!unitsMapRev[value.units]) throw new Error(`Invalid units: ${value.units} in ${key}`);
writeSignature(writer, unitsMapRev[value.units]);
writeFloat32(writer, value.value);
break;
case 'TEXT': // String
writeUnicodeStringWithPadding(writer, value);
break;
case 'enum': { // Enumerated
if (typeof value !== 'string') throw new Error(`Invalid enum value: ${JSON.stringify(value)}, key: ${key}`);
const [_type, val] = value.split('.');
writeAsciiStringOrClassId(writer, _type);
writeAsciiStringOrClassId(writer, val);
break;
}
case 'long': // Integer
if (typeof value !== 'number') throw new Error(`Invalid integer value: ${JSON.stringify(value)}, key: ${key}`);
writeInt32(writer, value);
break;
// case 'comp': // Large Integer
// writeLargeInteger(reader);
case 'bool': // Boolean
if (typeof value !== 'boolean') throw new Error(`Invalid boolean value: ${JSON.stringify(value)}, key: ${key}`);
writeUint8(writer, value ? 1 : 0);
break;
// case 'type': // Class
// case 'GlbC': // Class
// writeClassStructure(reader);
// case 'alis': // Alias
// writeAliasStructure(reader);
case 'tdta': // Raw Data
writeInt32(writer, value.byteLength);
writeBytes(writer, value);
break;
case 'ObAr': { // Object array
writeInt32(writer, 16); // version
writeUnicodeStringWithPadding(writer, ''); // name
const type = ObArTypes[key];
if (!type) throw new Error(`Not implemented ObArType for: ${key}`);
writeAsciiStringOrClassId(writer, type);
writeInt32(writer, value.length);
for (let i = 0; i < value.length; i++) {
writeAsciiStringOrClassId(writer, value[i].type); // Hrzn | Vrtc
writeSignature(writer, 'UnFl');
writeSignature(writer, '#Pxl');
writeInt32(writer, value[i].values.length);
for (let j = 0; j < value[i].values.length; j++) {
writeFloat64(writer, value[i].values[j]);
}
}
break;
}
case 'Pth ': { // File path
const length = 4 + 4 + 4 + value.path.length * 2;
writeInt32(writer, length);
writeSignature(writer, value.sig);
writeInt32LE(writer, length);
writeInt32LE(writer, value.path.length);
writeUnicodeStringWithoutLengthLE(writer, value.path);
break;
}
default:
throw new Error(`Not implemented descriptor OSType: ${type}`);
}
}
function readReferenceStructure(reader: PsdReader) {
const itemsCount = readInt32(reader);
const items: any[] = [];
for (let i = 0; i < itemsCount; i++) {
const type = readSignature(reader);
switch (type) {
case 'prop': { // Property
readClassStructure(reader);
const keyID = readAsciiStringOrClassId(reader);
items.push(keyID);
break;
}
case 'Clss': // Class
items.push(readClassStructure(reader));
break;
case 'Enmr': { // Enumerated Reference
readClassStructure(reader);
const typeID = readAsciiStringOrClassId(reader);
const value = readAsciiStringOrClassId(reader);
items.push(`${typeID}.${value}`);
break;
}
case 'rele': { // Offset
// const { name, classID } =
readClassStructure(reader);
items.push(readUint32(reader));
break;
}
case 'Idnt': // Identifier
items.push(readInt32(reader));
break;
case 'indx': // Index
items.push(readInt32(reader));
break;
case 'name': { // Name
readClassStructure(reader);
items.push(readUnicodeString(reader));
break;
}
default:
throw new Error(`Invalid descriptor reference type: ${type}`);
}
}
return items;
}
function writeReferenceStructure(writer: PsdWriter, _key: string, items: any[]) {
writeInt32(writer, items.length);
for (let i = 0; i < items.length; i++) {
const value = items[i];
let type = 'unknown';
if (typeof value === 'string') {
if (/^[a-z ]+\.[a-z ]+$/i.test(value)) {
type = 'Enmr';
} else {
type = 'name';
}
}
writeSignature(writer, type);
switch (type) {
// case 'prop': // Property
// case 'Clss': // Class
case 'Enmr': { // Enumerated Reference
const [typeID, enumValue] = value.split('.');
writeClassStructure(writer, '\0', typeID);
writeAsciiStringOrClassId(writer, typeID);
writeAsciiStringOrClassId(writer, enumValue);
break;
}
// case 'rele': // Offset
// case 'Idnt': // Identifier
// case 'indx': // Index
case 'name': { // Name
writeClassStructure(writer, '\0', 'Lyr ');
writeUnicodeString(writer, value + '\0');
break;
}
default:
throw new Error(`Invalid descriptor reference type: ${type}`);
}
}
return items;
}
function readClassStructure(reader: PsdReader) {
const name = readUnicodeString(reader);
const classID = readAsciiStringOrClassId(reader);
return { name, classID };
}
function writeClassStructure(writer: PsdWriter, name: string, classID: string) {
writeUnicodeString(writer, name);
writeAsciiStringOrClassId(writer, classID);
}
export function readVersionAndDescriptor(reader: PsdReader, includeClass = false) {
const version = readUint32(reader);
if (version !== 16) throw new Error(`Invalid descriptor version: ${version}`);
const desc = readDescriptorStructure(reader, includeClass);
// console.log(require('util').inspect(desc, false, 99, true));
return desc;
}
export function writeVersionAndDescriptor(writer: PsdWriter, name: string, classID: string, descriptor: any, root = '') {
writeUint32(writer, 16); // version
writeDescriptorStructure(writer, name, classID, descriptor, root);
}
export type DescriptorUnits = 'Angle' | 'Density' | 'Distance' | 'None' | 'Percent' | 'Pixels' |
'Millimeters' | 'Points' | 'Picas' | 'Inches' | 'Centimeters';
export interface DescriptorUnitsValue {
units: DescriptorUnits;
value: number;
}
export type DescriptorColor = {
_name: '';
_classID: 'RGBC';
'Rd ': number;
'Grn ': number;
'Bl ': number;
} | {
_name: '';
_classID: 'HSBC'; // ???
'H ': DescriptorUnitsValue;
Strt: number;
Brgh: number;
} | {
_name: '';
_classID: 'CMYC';
'Cyn ': number;
Mgnt: number;
'Ylw ': number;
Blck: number;
} | {
_name: '';
_classID: 'GRYC'; // ???
'Gry ': number;
} | {
_name: '';
_classID: 'LABC'; // ???
Lmnc: number;
'A ': number;
'B ': number;
} | {
_name: '';
_classID: 'RGBC';
redFloat: number;
greenFloat: number;
blueFloat: number;
};
export interface DesciptorPattern {
'Nm ': string;
Idnt: string;
}
export type DesciptorGradient = {
'Nm ': string;
GrdF: 'GrdF.CstS';
Intr: number;
Clrs: {
'Clr ': DescriptorColor;
Type: 'Clry.UsrS';
Lctn: number;
Mdpn: number;
}[];
Trns: {
Opct: DescriptorUnitsValue;
Lctn: number;
Mdpn: number;
}[];
} | {
GrdF: 'GrdF.ClNs';
Smth: number;
'Nm ': string;
ClrS: string;
RndS: number;
VctC?: boolean;
ShTr?: boolean;
'Mnm ': number[];
'Mxm ': number[];
};
export interface DescriptorColorContent {
'Clr ': DescriptorColor;
}
export interface DescriptorGradientContent {
Dthr?: boolean;
gradientsInterpolationMethod?: string; // 'gradientInterpolationMethodType.Smoo'
Angl?: DescriptorUnitsValue;
Type: string;
Grad: DesciptorGradient;
Rvrs?: boolean;
'Scl '?: DescriptorUnitsValue;
Algn?: boolean;
Ofst?: { Hrzn: DescriptorUnitsValue; Vrtc: DescriptorUnitsValue; };
}
export interface DescriptorPatternContent {
Ptrn: DesciptorPattern;
Lnkd?: boolean;
phase?: { Hrzn: number; Vrtc: number; };
}
export type DescriptorVectorContent = DescriptorColorContent | DescriptorGradientContent | DescriptorPatternContent;
export interface StrokeDescriptor {
strokeStyleVersion: number;
strokeEnabled: boolean;
fillEnabled: boolean;
strokeStyleLineWidth: DescriptorUnitsValue;
strokeStyleLineDashOffset: DescriptorUnitsValue;
strokeStyleMiterLimit: number;
strokeStyleLineCapType: string;
strokeStyleLineJoinType: string;
strokeStyleLineAlignment: string;
strokeStyleScaleLock: boolean;
strokeStyleStrokeAdjust: boolean;
strokeStyleLineDashSet: DescriptorUnitsValue[];
strokeStyleBlendMode: string;
strokeStyleOpacity: DescriptorUnitsValue;
strokeStyleContent: DescriptorVectorContent;
strokeStyleResolution: number;
}
export interface BoundsDescriptor {
Left: DescriptorUnitsValue;
'Top ': DescriptorUnitsValue;
Rght: DescriptorUnitsValue;
Btom: DescriptorUnitsValue;
}
export interface TextDescriptor {
'Txt ': string;
textGridding: string;
Ornt: string;
AntA: string;
bounds?: BoundsDescriptor;
boundingBox?: BoundsDescriptor;
TextIndex: number;
EngineData?: Uint8Array;
}
export interface WarpDescriptor {
warpStyle: string;
warpValue?: number;
warpValues?: number[]
warpPerspective: number;
warpPerspectiveOther: number;
warpRotate: string;
bounds?: {
'Top ': DescriptorUnitsValue;
Left: DescriptorUnitsValue;
Btom: DescriptorUnitsValue;
Rght: DescriptorUnitsValue;
} | {
_classID: 'classFloatRect',
'Top ': number,
Left: number,
Btom: number,
Rght: number,
},
uOrder: number;
vOrder: number;
customEnvelopeWarp?: {
_name: '';
_classID: 'customEnvelopeWarp';
meshPoints: {
type: 'Hrzn' | 'Vrtc';
values: number[];
}[];
};
}
export interface QuiltWarpDescriptor extends WarpDescriptor {
deformNumRows: number;
deformNumCols: number;
customEnvelopeWarp: {
_name: '';
_classID: 'customEnvelopeWarp';
quiltSliceX: {
type: 'quiltSliceX';
values: number[];
}[];
quiltSliceY: {
type: 'quiltSliceY';
values: number[];
}[];
meshPoints: {
type: 'Hrzn' | 'Vrtc';
values: number[];
}[];
};
}
export interface FractionDescriptor {
numerator: number;
denominator: number;
}
export interface HrznVrtcDescriptor {
Hrzn: number;
Vrtc: number;
}
export interface FrameDescriptor {
FrLs: number[];
enab?: boolean;
IMsk?: { Ofst: HrznVrtcDescriptor };
VMsk?: { Ofst: HrznVrtcDescriptor };
Ofst?: HrznVrtcDescriptor;
FXRf?: HrznVrtcDescriptor;
Lefx?: Lfx2Descriptor;
blendOptions?: { Opct: DescriptorUnitsValue; };
}
export interface FrameListDescriptor {
LaID: number; // layer ID
LaSt: FrameDescriptor[];
}
export function horzVrtcToXY(hv: HrznVrtcDescriptor): { x: number; y: number; } {
return { x: hv.Hrzn, y: hv.Vrtc };
}
export function xyToHorzVrtc(xy: { x: number; y: number; }): HrznVrtcDescriptor {
return { Hrzn: xy.x, Vrtc: xy.y };
}
export function descBoundsToBounds(desc: BoundsDescriptor): UnitsBounds {
return {
top: parseUnits(desc['Top ']),
left: parseUnits(desc.Left),
right: parseUnits(desc.Rght),
bottom: parseUnits(desc.Btom),
};
}
export function boundsToDescBounds(bounds: UnitsBounds): BoundsDescriptor {
return {
Left: unitsValue(bounds.left, 'bounds.left'),
['Top ']: unitsValue(bounds.top, 'bounds.top'),
Rght: unitsValue(bounds.right, 'bounds.right'),
Btom: unitsValue(bounds.bottom, 'bounds.bottom'),
};
}
export type TimelineAnimKeyDescriptor = {
Type: 'keyType.Opct';
Opct: DescriptorUnitsValue;
} | {
Type: 'keyType.Trnf';
'Scl ': HrznVrtcDescriptor;
Skew: HrznVrtcDescriptor;
rotation: number;
translation: HrznVrtcDescriptor;
} | {
Type: 'keyType.Pstn';
Hrzn: number;
Vrtc: number;
} | {
Type: 'keyType.sheetStyle';
sheetStyle: {
Vrsn: number;
Lefx?: Lfx2Descriptor;
blendOptions: {};
};
} | {
Type: 'keyType.globalLighting';
gblA: number;
globalAltitude: number;
};
export interface TimelineKeyDescriptor {
Vrsn: 1;
animInterpStyle: 'animInterpStyle.Lnr ' | 'animInterpStyle.hold';
time: FractionDescriptor;
animKey: TimelineAnimKeyDescriptor;
selected: boolean;
}
export interface TimelineTrackDescriptor {
trackID: 'stdTrackID.globalLightingTrack' | 'stdTrackID.opacityTrack' | 'stdTrackID.styleTrack' | 'stdTrackID.sheetTransformTrack' | 'stdTrackID.sheetPositionTrack';
Vrsn: 1;
enab: boolean;
Effc: boolean;
effectParams?: {
keyList: TimelineKeyDescriptor[];
fillCanvas: boolean;
zoomOrigin: number;
};
keyList: TimelineKeyDescriptor[];
}
export interface TimeScopeDescriptor {
Vrsn: 1;
Strt: FractionDescriptor;
duration: FractionDescriptor;
inTime: FractionDescriptor;
outTime: FractionDescriptor;
}
export interface TimelineDescriptor {
Vrsn: 1;
timeScope: TimeScopeDescriptor;
autoScope: boolean;
audioLevel: number;
LyrI: number;
trackList?: TimelineTrackDescriptor[];
}
export interface EffectDescriptor extends Partial<DescriptorGradientContent>, Partial<DescriptorPatternContent> {
enab?: boolean;
Styl: string;
PntT?: string;
'Md '?: string;
Opct?: DescriptorUnitsValue;
'Sz '?: DescriptorUnitsValue;
'Clr '?: DescriptorColor;
present?: boolean;
showInDialog?: boolean;
overprint?: boolean;
uglg?: boolean; // useGlobalLight
// more fields here used in parseEffectObject
}
export interface Lfx2Descriptor {
'Scl '?: DescriptorUnitsValue;
masterFXSwitch?: boolean;
DrSh?: EffectDescriptor;
IrSh?: EffectDescriptor;
OrGl?: EffectDescriptor;
IrGl?: EffectDescriptor;
ebbl?: EffectDescriptor;
SoFi?: EffectDescriptor;
patternFill?: EffectDescriptor;
GrFl?: EffectDescriptor;
ChFX?: EffectDescriptor;
FrFX?: EffectDescriptor;
}
export interface LmfxDescriptor {
'Scl '?: DescriptorUnitsValue;
masterFXSwitch?: boolean;
dropShadowMulti?: EffectDescriptor[];
innerShadowMulti?: EffectDescriptor[];
OrGl?: EffectDescriptor;
solidFillMulti?: EffectDescriptor[];
gradientFillMulti?: EffectDescriptor[];
patternFill?: EffectDescriptor; // ???
frameFXMulti?: EffectDescriptor[];
IrGl?: EffectDescriptor;
ebbl?: EffectDescriptor;
ChFX?: EffectDescriptor;
numModifyingFX?: number; // number of effects with enabled = true
}
function parseFxObject(fx: EffectDescriptor) {
const stroke: LayerEffectStroke = {
enabled: !!fx.enab,
position: FStl.decode(fx.Styl),
fillType: FrFl.decode(fx.PntT!),
blendMode: BlnM.decode(fx['Md ']!),
opacity: parsePercent(fx.Opct),
size: parseUnits(fx['Sz ']!),
};
if (fx.present !== undefined) stroke.present = fx.present;
if (fx.showInDialog !== undefined) stroke.showInDialog = fx.showInDialog;
if (fx.overprint !== undefined) stroke.overprint = fx.overprint;
if (fx['Clr ']) stroke.color = parseColor(fx['Clr ']);
if (fx.Grad) stroke.gradient = parseGradientContent(fx as any);
if (fx.Ptrn) stroke.pattern = parsePatternContent(fx as any);
return stroke;
}
function serializeFxObject(stroke: LayerEffectStroke) {
let FrFX: EffectDescriptor = {} as any;
FrFX.enab = !!stroke.enabled;
if (stroke.present !== undefined) FrFX.present = !!stroke.present;
if (stroke.showInDialog !== undefined) FrFX.showInDialog = !!stroke.showInDialog;
FrFX.Styl = FStl.encode(stroke.position);
FrFX.PntT = FrFl.encode(stroke.fillType);
FrFX['Md '] = BlnM.encode(stroke.blendMode);
FrFX.Opct = unitsPercent(stroke.opacity);
FrFX['Sz '] = unitsValue(stroke.size, 'size');
if (stroke.color) FrFX['Clr '] = serializeColor(stroke.color);
if (stroke.gradient) FrFX = { ...FrFX, ...serializeGradientContent(stroke.gradient) };
if (stroke.pattern) FrFX = { ...FrFX, ...serializePatternContent(stroke.pattern) };
if (stroke.overprint !== undefined) FrFX.overprint = !!stroke.overprint;
return FrFX;
}
export function serializeEffects(e: LayerEffectsInfo, log: boolean, multi: boolean) {
const info: Lfx2Descriptor & LmfxDescriptor = multi ? {
'Scl ': unitsPercentF(e.scale ?? 1),
masterFXSwitch: !e.disabled,
} : {
masterFXSwitch: !e.disabled,
'Scl ': unitsPercentF(e.scale ?? 1),
};
const arrayKeys: (keyof LayerEffectsInfo)[] = ['dropShadow', 'innerShadow', 'solidFill', 'gradientOverlay', 'stroke'];
for (const key of arrayKeys) {
if (e[key] && !Array.isArray(e[key])) throw new Error(`${key} should be an array`);
}
const useMulti = <T>(arr: undefined | T[]): arr is T[] => !!arr && arr.length > 1 && multi;
const useSingle = <T>(arr: undefined | T[]): arr is T[] => !!arr && arr.length >= 1 && (!multi || arr.length === 1);
if (useSingle(e.dropShadow)) info.DrSh = serializeEffectObject(e.dropShadow[0], 'dropShadow', log);
if (useMulti(e.dropShadow)) info.dropShadowMulti = e.dropShadow.map(i => serializeEffectObject(i, 'dropShadow', log));
if (useSingle(e.innerShadow)) info.IrSh = serializeEffectObject(e.innerShadow[0], 'innerShadow', log);
if (useMulti(e.innerShadow)) info.innerShadowMulti = e.innerShadow.map(i => serializeEffectObject(i, 'innerShadow', log));
if (e.outerGlow) info.OrGl = serializeEffectObject(e.outerGlow, 'outerGlow', log);
if (useMulti(e.solidFill)) info.solidFillMulti = e.solidFill.map(i => serializeEffectObject(i, 'solidFill', log));
if (useMulti(e.gradientOverlay)) info.gradientFillMulti = e.gradientOverlay.map(i => serializeEffectObject(i, 'gradientOverlay', log));
if (useMulti(e.stroke)) info.frameFXMulti = e.stroke.map(i => serializeFxObject(i));
if (e.innerGlow) info.IrGl = serializeEffectObject(e.innerGlow, 'innerGlow', log);
if (e.bevel) info.ebbl = serializeEffectObject(e.bevel, 'bevel', log);
if (useSingle(e.solidFill)) info.SoFi = serializeEffectObject(e.solidFill[0], 'solidFill', log);
if (e.patternOverlay) info.patternFill = serializeEffectObject(e.patternOverlay, 'patternOverlay', log);
if (useSingle(e.gradientOverlay)) info.GrFl = serializeEffectObject(e.gradientOverlay[0], 'gradientOverlay', log);
if (e.satin) info.ChFX = serializeEffectObject(e.satin, 'satin', log);
if (useSingle(e.stroke)) info.FrFX = serializeFxObject(e.stroke?.[0]);
if (multi) {
info.numModifyingFX = 0;
for (const key of Object.keys(e)) {
const value = (e as any)[key];
if (Array.isArray(value)) {
for (const effect of value) {
if (effect.enabled) info.numModifyingFX++;
}
} else if (value.enabled) {
info.numModifyingFX++;
}
}
}
return info;
}
export function parseEffects(info: Lfx2Descriptor & LmfxDescriptor, log: boolean) {
const effects: LayerEffectsInfo = {};
const { masterFXSwitch, DrSh, dropShadowMulti, IrSh, innerShadowMulti, OrGl, IrGl, ebbl, SoFi, solidFillMulti, patternFill, GrFl, gradientFillMulti, ChFX, FrFX, frameFXMulti, numModifyingFX, ...rest } = info;
if (!masterFXSwitch) effects.disabled = true;
if (info['Scl ']) effects.scale = parsePercent(info['Scl ']);
if (DrSh) effects.dropShadow = [parseEffectObject(DrSh, log)];
if (dropShadowMulti) effects.dropShadow = dropShadowMulti.map(i => parseEffectObject(i, log));
if (IrSh) effects.innerShadow = [parseEffectObject(IrSh, log)];
if (innerShadowMulti) effects.innerShadow = innerShadowMulti.map(i => parseEffectObject(i, log));
if (OrGl) effects.outerGlow = parseEffectObject(OrGl, log);
if (IrGl) effects.innerGlow = parseEffectObject(IrGl, log);
if (ebbl) effects.bevel = parseEffectObject(ebbl, log);
if (SoFi) effects.solidFill = [parseEffectObject(SoFi, log)];
if (solidFillMulti) effects.solidFill = solidFillMulti.map(i => parseEffectObject(i, log));
if (patternFill) effects.patternOverlay = parseEffectObject(patternFill, log);
if (GrFl) effects.gradientOverlay = [parseEffectObject(GrFl, log)];
if (gradientFillMulti) effects.gradientOverlay = gradientFillMulti.map(i => parseEffectObject(i, log));
if (ChFX) effects.satin = parseEffectObject(ChFX, log);
if (FrFX) effects.stroke = [parseFxObject(FrFX)];
if (frameFXMulti) effects.stroke = frameFXMulti.map(i => parseFxObject(i));
if (log && Object.keys(rest).length > 1) console.log('Unhandled effect keys:', rest);
return effects;
}
function parseKeyList(keyList: TimelineKeyDescriptor[], logMissingFeatures: boolean) {
const keys: TimelineKey[] = [];
for (let j = 0; j < keyList.length; j++) {
const key = keyList[j];
const { time: { denominator, numerator }, selected, animKey } = key;
const time = { numerator, denominator };
const interpolation = animInterpStyleEnum.decode(key.animInterpStyle);
switch (animKey.Type) {
case 'keyType.Opct':
keys.push({ interpolation, time, selected, type: 'opacity', value: parsePercent(animKey.Opct) });
break;
case 'keyType.Pstn':
keys.push({ interpolation, time, selected, type: 'position', x: animKey.Hrzn, y: animKey.Vrtc });
break;
case 'keyType.Trnf':
keys.push({
interpolation, time, selected, type: 'transform',
scale: horzVrtcToXY(animKey['Scl ']), skew: horzVrtcToXY(animKey.Skew), rotation: animKey.rotation, translation: horzVrtcToXY(animKey.translation)
});
break;
case 'keyType.sheetStyle': {
const key: TimelineKey = { interpolation, time, selected, type: 'style' };
if (animKey.sheetStyle.Lefx) key.style = parseEffects(animKey.sheetStyle.Lefx, logMissingFeatures);
keys.push(key);
break;
}
case 'keyType.globalLighting': {
keys.push({
interpolation, time, selected, type: 'globalLighting',
globalAngle: animKey.gblA, globalAltitude: animKey.globalAltitude
});
break;
}
default: throw new Error(`Unsupported keyType value`);
}
}
return keys;
}
function serializeKeyList(keys: TimelineKey[]): TimelineKeyDescriptor[] {
const keyList: TimelineKeyDescriptor[] = [];
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
const { time, selected = false, interpolation } = key;
const animInterpStyle = animInterpStyleEnum.encode(interpolation) as 'animInterpStyle.Lnr ' | 'animInterpStyle.hold';
let animKey: TimelineAnimKeyDescriptor;
switch (key.type) {
case 'opacity':
animKey = { Type: 'keyType.Opct', Opct: unitsPercent(key.value) };
break;
case 'position':
animKey = { Type: 'keyType.Pstn', Hrzn: key.x, Vrtc: key.y };
break;
case 'transform':
animKey = { Type: 'keyType.Trnf', 'Scl ': xyToHorzVrtc(key.scale), Skew: xyToHorzVrtc(key.skew), rotation: key.rotation, translation: xyToHorzVrtc(key.translation) };
break;
case 'style':
animKey = { Type: 'keyType.sheetStyle', sheetStyle: { Vrsn: 1, blendOptions: {} } };
if (key.style) animKey.sheetStyle = { Vrsn: 1, Lefx: serializeEffects(key.style, false, false), blendOptions: {} };
break;
case 'globalLighting': {
animKey = { Type: 'keyType.globalLighting', gblA: key.globalAngle, globalAltitude: key.globalAltitude };
break;
}
default: throw new Error(`Unsupported keyType value`);
}
keyList.push({ Vrsn: 1, animInterpStyle, time, animKey, selected });
}
return keyList;
}
export function parseTrackList(trackList: TimelineTrackDescriptor[], logMissingFeatures: boolean) {
const tracks: TimelineTrack[] = [];
for (let i = 0; i < trackList.length; i++) {
const tr = trackList[i];
const track: TimelineTrack = {
type: stdTrackID.decode(tr.trackID),
enabled: tr.enab,
keys: parseKeyList(tr.keyList, logMissingFeatures),
};
if (tr.effectParams) {
track.effectParams = {
fillCanvas: tr.effectParams.fillCanvas,
zoomOrigin: tr.effectParams.zoomOrigin,
keys: parseKeyList(tr.effectParams.keyList, logMissingFeatures),
};
}
tracks.push(track);
}
return tracks;
}
export function serializeTrackList(tracks: TimelineTrack[]): TimelineTrackDescriptor[] {
const trackList: TimelineTrackDescriptor[] = [];
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
trackList.push({
trackID: stdTrackID.encode(t.type) as any,
Vrsn: 1,
enab: !!t.enabled,
Effc: !!t.effectParams,
...(t.effectParams ? {
effectParams: {
keyList: serializeKeyList(t.keys),
fillCanvas: t.effectParams.fillCanvas,
zoomOrigin: t.effectParams.zoomOrigin,
}
} : {}),
keyList: serializeKeyList(t.keys),
});
}
return trackList;
}
type AllEffects = LayerEffectShadow & LayerEffectsOuterGlow & LayerEffectStroke &
LayerEffectInnerGlow & LayerEffectBevel & LayerEffectSolidFill &
LayerEffectPatternOverlay & LayerEffectSatin & LayerEffectGradientOverlay;
function parseEffectObject(obj: any, reportErrors: boolean) {
const result: AllEffects = {} as any;
for (const key of Object.keys(obj)) {
const val = obj[key];
switch (key) {
case 'enab': result.enabled = !!val; break;
case 'uglg': result.useGlobalLight = !!val; break;
case 'AntA': result.antialiased = !!val; break;
case 'Algn': result.align = !!val; break;
case 'Dthr': result.dither = !!val; break;
case 'Invr': result.invert = !!val; break;
case 'Rvrs': result.reverse = !!val; break;
case 'Clr ': result.color = parseColor(val); break;
case 'hglC': result.highlightColor = parseColor(val); break;
case 'sdwC': result.shadowColor = parseColor(val); break;
case 'Styl': result.position = FStl.decode(val); break;
case 'Md ': result.blendMode = BlnM.decode(val); break;
case 'hglM': result.highlightBlendMode = BlnM.decode(val); break;
case 'sdwM': result.shadowBlendMode = BlnM.decode(val); break;
case 'bvlS': result.style = BESl.decode(val); break;
case 'bvlD': result.direction = BESs.decode(val); break;
case 'bvlT': result.technique = bvlT.decode(val) as any; break;
case 'GlwT': result.technique = BETE.decode(val) as any; break;
case 'glwS': result.source = IGSr.decode(val); break;
case 'Type': result.type = GrdT.decode(val); break;
case 'gs99': result.interpolationMethod = gradientInterpolationMethodType.decode(val); break;
case 'Opct': result.opacity = parsePercent(val); break;
case 'hglO': result.highlightOpacity = parsePercent(val); break;
case 'sdwO': result.shadowOpacity = parsePercent(val); break;
case 'lagl': result.angle = parseAngle(val); break;
case 'Angl': result.angle = parseAngle(val); break;
case 'Lald': result.altitude = parseAngle(val); break;
case 'Sftn': result.soften = p