import { fromByteArray, toByteArray } from 'base64-js'; import { readEffects, writeEffects } from './effectsHelpers'; import { clamp, createEnum, layerColors, MOCK_HANDLERS } from './helpers'; import { LayerAdditionalInfo, BezierPath, Psd, BrightnessAdjustment, ExposureAdjustment, VibranceAdjustment, ColorBalanceAdjustment, BlackAndWhiteAdjustment, PhotoFilterAdjustment, ChannelMixerChannel, ChannelMixerAdjustment, PosterizeAdjustment, ThresholdAdjustment, GradientMapAdjustment, CMYK, SelectiveColorAdjustment, ColorLookupAdjustment, LevelsAdjustmentChannel, LevelsAdjustment, CurvesAdjustment, CurvesAdjustmentChannel, HueSaturationAdjustment, HueSaturationAdjustmentChannel, PresetInfo, Color, ColorBalanceValues, WriteOptions, LinkedFile, PlacedLayerType, Warp, KeyDescriptorItem, BooleanOperation, LayerEffectsInfo, Annotation, LayerVectorMask, AnimationFrame, Timeline, PlacedLayerFilter, UnitsValue, Filter, PlacedLayer, ReadOptions, Layer } from './psd'; import { PsdReader, readSignature, readUnicodeString, skipBytes, readUint32, readUint8, readFloat64, readUint16, readBytes, readInt16, checkSignature, readFloat32, readFixedPointPath32, readSection, readColor, readInt32, readPascalString, readUnicodeStringWithLength, readAsciiString, readPattern, readLayerInfo } from './psdReader'; import { PsdWriter, writeZeros, writeSignature, writeBytes, writeUint32, writeUint16, writeFloat64, writeUint8, writeInt16, writeFloat32, writeFixedPointPath32, writeUnicodeString, writeSection, writeUnicodeStringWithPadding, writeColor, writePascalString, writeInt32 } from './psdWriter'; import { Annt, BlnM, DescriptorColor, DescriptorUnitsValue, parsePercent, parseUnits, parseUnitsOrNumber, QuiltWarpDescriptor, strokeStyleLineAlignment, strokeStyleLineCapType, strokeStyleLineJoinType, TextDescriptor, textGridding, unitsPercent, unitsValue, WarpDescriptor, warpStyle, writeVersionAndDescriptor, readVersionAndDescriptor, StrokeDescriptor, Ornt, horzVrtcToXY, LmfxDescriptor, Lfx2Descriptor, FrameListDescriptor, TimelineDescriptor, FrameDescriptor, xyToHorzVrtc, serializeEffects, parseEffects, parseColor, serializeColor, serializeVectorContent, parseVectorContent, parseTrackList, serializeTrackList, FractionDescriptor, BlrM, BlrQ, SmBQ, SmBM, DspM, UndA, Cnvr, RplS, SphM, Wvtp, ZZTy, Dstr, Chnl, MztT, Lns, blurType, DfsM, ExtT, ExtR, FlCl, CntE, WndM, Drct, IntE, IntC, FlMd, unitsPercentF, frac, ClrS, descBoundsToBounds, boundsToDescBounds, presetKindType, gradientInterpolationMethodType, PxScDescriptor } from './descriptor'; import { serializeEngineData, parseEngineData } from './engineData'; import { encodeEngineData, decodeEngineData } from './text'; import { decodeEngineData2 } from './engineData2'; import type { InternalImageResources } from './imageResources'; export interface ExtendedWriteOptions extends WriteOptions { layerIds: Set; layerToId: Map; } type HasMethod = (target: LayerAdditionalInfo) => boolean; type ReadMethod = (reader: PsdReader, target: LayerAdditionalInfo, left: () => number, psd: Psd, imageResources: InternalImageResources) => void; type WriteMethod = (writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) => void; export interface InfoHandler { key: string; has: HasMethod; read: ReadMethod; write: WriteMethod; } const fromAtoZ = 'abcdefghijklmnopqrstuvwxyz'; export const infoHandlers: InfoHandler[] = []; export const infoHandlersMap: { [key: string]: InfoHandler; } = {}; function addHandler(key: string, has: HasMethod, read: ReadMethod, write: WriteMethod) { const handler: InfoHandler = { key, has, read, write }; infoHandlers.push(handler); infoHandlersMap[handler.key] = handler; } function addHandlerAlias(key: string, target: string) { infoHandlersMap[key] = infoHandlersMap[target]; } function hasKey(key: keyof LayerAdditionalInfo) { return (target: LayerAdditionalInfo) => target[key] !== undefined; } function readLength64(reader: PsdReader) { if (readUint32(reader)) throw new Error(`Resource size above 4 GB limit at ${reader.offset.toString(16)}`); return readUint32(reader); } function writeLength64(writer: PsdWriter, length: number) { writeUint32(writer, 0); writeUint32(writer, length); } addHandler( 'TySh', hasKey('text'), (reader, target, leftBytes) => { if (readInt16(reader) !== 1) throw new Error(`Invalid TySh version`); const transform: number[] = []; for (let i = 0; i < 6; i++) transform.push(readFloat64(reader)); if (readInt16(reader) !== 50) throw new Error(`Invalid TySh text version`); const text: TextDescriptor = readVersionAndDescriptor(reader); // console.log(require('util').inspect(text, false, 99, false), 'utf8'); if (readInt16(reader) !== 1) throw new Error(`Invalid TySh warp version`); const warp: WarpDescriptor = readVersionAndDescriptor(reader); // console.log(require('util').inspect(warp, false, 99, false), 'utf8'); target.text = { transform, left: readFloat32(reader), top: readFloat32(reader), right: readFloat32(reader), bottom: readFloat32(reader), text: text['Txt '].replace(/\r/g, '\n'), index: text.TextIndex || 0, gridding: textGridding.decode(text.textGridding), antiAlias: Annt.decode(text.AntA), orientation: Ornt.decode(text.Ornt), warp: { style: warpStyle.decode(warp.warpStyle), value: warp.warpValue || 0, perspective: warp.warpPerspective || 0, perspectiveOther: warp.warpPerspectiveOther || 0, rotate: Ornt.decode(warp.warpRotate), }, }; if (text.bounds) target.text.bounds = descBoundsToBounds(text.bounds); if (text.boundingBox) target.text.boundingBox = descBoundsToBounds(text.boundingBox); if (text.EngineData) { const engineData = parseEngineData(text.EngineData); const textData = decodeEngineData(engineData); // console.log(require('util').inspect(engineData, false, 99, false), 'utf8'); // require('fs').writeFileSync(`layer-${target.name}.txt`, require('util').inspect(engineData, false, 99, false), 'utf8'); // const before = parseEngineData(text.EngineData); // const after = encodeEngineData(engineData); // require('fs').writeFileSync('before.txt', require('util').inspect(before, false, 99, false), 'utf8'); // require('fs').writeFileSync('after.txt', require('util').inspect(after, false, 99, false), 'utf8'); // console.log(require('util').inspect(parseEngineData(text.EngineData), false, 99, true)); target.text = { ...target.text, ...textData }; // console.log(require('util').inspect(target.text, false, 99, true)); } skipBytes(reader, leftBytes()); }, (writer, target) => { const text = target.text!; const warp = text.warp || {}; const transform = text.transform || [1, 0, 0, 1, 0, 0]; const textDescriptor: TextDescriptor = { 'Txt ': (text.text || '').replace(/\r?\n/g, '\r'), textGridding: textGridding.encode(text.gridding), Ornt: Ornt.encode(text.orientation), AntA: Annt.encode(text.antiAlias), ...(text.bounds ? { bounds: boundsToDescBounds(text.bounds) } : {}), ...(text.boundingBox ? { boundingBox: boundsToDescBounds(text.boundingBox) } : {}), TextIndex: text.index || 0, EngineData: serializeEngineData(encodeEngineData(text)), }; writeInt16(writer, 1); // version for (let i = 0; i < 6; i++) { writeFloat64(writer, transform[i]); } writeInt16(writer, 50); // text version writeVersionAndDescriptor(writer, '', 'TxLr', textDescriptor, 'text'); writeInt16(writer, 1); // warp version writeVersionAndDescriptor(writer, '', 'warp', encodeWarp(warp)); writeFloat32(writer, text.left!); writeFloat32(writer, text.top!); writeFloat32(writer, text.right!); writeFloat32(writer, text.bottom!); // writeZeros(writer, 2); }, ); // vector fills addHandler( 'SoCo', target => target.vectorFill !== undefined && target.vectorStroke === undefined && target.vectorFill.type === 'color', (reader, target) => { const descriptor = readVersionAndDescriptor(reader); target.vectorFill = parseVectorContent(descriptor); }, (writer, target) => { const { descriptor } = serializeVectorContent(target.vectorFill!); writeVersionAndDescriptor(writer, '', 'null', descriptor); }, ); addHandler( 'GdFl', target => target.vectorFill !== undefined && target.vectorStroke === undefined && (target.vectorFill.type === 'solid' || target.vectorFill.type === 'noise'), (reader, target, left) => { const descriptor = readVersionAndDescriptor(reader); target.vectorFill = parseVectorContent(descriptor); skipBytes(reader, left()); }, (writer, target) => { const { descriptor } = serializeVectorContent(target.vectorFill!); writeVersionAndDescriptor(writer, '', 'null', descriptor); }, ); addHandler( 'PtFl', target => target.vectorFill !== undefined && target.vectorStroke === undefined && target.vectorFill.type === 'pattern', (reader, target) => { const descriptor = readVersionAndDescriptor(reader); target.vectorFill = parseVectorContent(descriptor); }, (writer, target) => { const { descriptor } = serializeVectorContent(target.vectorFill!); writeVersionAndDescriptor(writer, '', 'null', descriptor); }, ); addHandler( 'vscg', target => target.vectorFill !== undefined && target.vectorStroke !== undefined, (reader, target, left) => { readSignature(reader); // key const desc = readVersionAndDescriptor(reader); target.vectorFill = parseVectorContent(desc); skipBytes(reader, left()); }, (writer, target) => { const { descriptor, key } = serializeVectorContent(target.vectorFill!); writeSignature(writer, key); writeVersionAndDescriptor(writer, '', 'null', descriptor); }, ); export function readBezierKnot(reader: PsdReader, width: number, height: number) { const y0 = readFixedPointPath32(reader) * height; const x0 = readFixedPointPath32(reader) * width; const y1 = readFixedPointPath32(reader) * height; const x1 = readFixedPointPath32(reader) * width; const y2 = readFixedPointPath32(reader) * height; const x2 = readFixedPointPath32(reader) * width; return [x0, y0, x1, y1, x2, y2]; } function writeBezierKnot(writer: PsdWriter, points: number[], width: number, height: number) { writeFixedPointPath32(writer, points[1] / height); // y0 writeFixedPointPath32(writer, points[0] / width); // x0 writeFixedPointPath32(writer, points[3] / height); // y1 writeFixedPointPath32(writer, points[2] / width); // x1 writeFixedPointPath32(writer, points[5] / height); // y2 writeFixedPointPath32(writer, points[4] / width); // x2 } export const booleanOperations: BooleanOperation[] = ['exclude', 'combine', 'subtract', 'intersect']; export function readVectorMask(reader: PsdReader, vectorMask: LayerVectorMask, width: number, height: number, size: number) { const end = reader.offset + size; const paths = vectorMask.paths; let path: BezierPath | undefined = undefined; while ((end - reader.offset) >= 26) { const selector = readUint16(reader); switch (selector) { case 0: // Closed subpath length record case 3: { // Open subpath length record readUint16(reader); // count const boolOp = readInt16(reader); const flags = readUint16(reader); // bit 1 always 1 ? skipBytes(reader, 18); path = { open: selector === 3, knots: [], fillRule: flags === 2 ? 'non-zero' : 'even-odd', }; if (boolOp !== -1) path.operation = booleanOperations[boolOp]; paths.push(path); break; } case 1: // Closed subpath Bezier knot, linked case 2: // Closed subpath Bezier knot, unlinked case 4: // Open subpath Bezier knot, linked case 5: // Open subpath Bezier knot, unlinked path!.knots.push({ linked: (selector === 1 || selector === 4), points: readBezierKnot(reader, width, height) }); break; case 6: // Path fill rule record skipBytes(reader, 24); break; case 7: { // Clipboard record // TODO: check if these need to be multiplied by document size const top = readFixedPointPath32(reader); const left = readFixedPointPath32(reader); const bottom = readFixedPointPath32(reader); const right = readFixedPointPath32(reader); const resolution = readFixedPointPath32(reader); skipBytes(reader, 4); vectorMask.clipboard = { top, left, bottom, right, resolution }; break; } case 8: // Initial fill rule record vectorMask.fillStartsWithAllPixels = !!readUint16(reader); skipBytes(reader, 22); break; default: throw new Error('Invalid vmsk section'); } } return paths; } addHandler( 'vmsk', hasKey('vectorMask'), (reader, target, left, { width, height }) => { if (readUint32(reader) !== 3) throw new Error('Invalid vmsk version'); target.vectorMask = { paths: [] }; const vectorMask = target.vectorMask; const flags = readUint32(reader); vectorMask.invert = (flags & 1) !== 0; vectorMask.notLink = (flags & 2) !== 0; vectorMask.disable = (flags & 4) !== 0; readVectorMask(reader, vectorMask, width, height, left()); // drawBezierPaths(vectorMask.paths, width, height, 'out.png'); skipBytes(reader, left()); }, (writer, target, { width, height }) => { const vectorMask = target.vectorMask!; const flags = (vectorMask.invert ? 1 : 0) | (vectorMask.notLink ? 2 : 0) | (vectorMask.disable ? 4 : 0); writeUint32(writer, 3); // version writeUint32(writer, flags); // initial entry writeUint16(writer, 6); writeZeros(writer, 24); const clipboard = vectorMask.clipboard; if (clipboard) { writeUint16(writer, 7); writeFixedPointPath32(writer, clipboard.top); writeFixedPointPath32(writer, clipboard.left); writeFixedPointPath32(writer, clipboard.bottom); writeFixedPointPath32(writer, clipboard.right); writeFixedPointPath32(writer, clipboard.resolution); writeZeros(writer, 4); } writeUint16(writer, 8); writeUint16(writer, vectorMask.fillStartsWithAllPixels ? 1 : 0); writeZeros(writer, 22); for (const path of vectorMask.paths) { writeUint16(writer, path.open ? 3 : 0); writeUint16(writer, path.knots.length); writeUint16(writer, path.operation ? booleanOperations.indexOf(path.operation) : -1); // -1 for undefined writeUint16(writer, path.fillRule === 'non-zero' ? 2 : 1); writeZeros(writer, 18); // TODO: these are sometimes non-zero const linkedKnot = path.open ? 4 : 1; const unlinkedKnot = path.open ? 5 : 2; for (const { linked, points } of path.knots) { writeUint16(writer, linked ? linkedKnot : unlinkedKnot); writeBezierKnot(writer, points, width, height); } } }, ); // TODO: need to write vmsk if has outline ? addHandlerAlias('vsms', 'vmsk'); // addHandlerAlias('vmsk', 'vsms'); addHandler( 'vowv', // something with vectors? hasKey('vowv'), (reader, target) => { target.vowv = readUint32(reader); // always 2 ???? }, (writer, target) => { writeUint32(writer, target.vowv!); }, ); interface VogkDescriptor { keyDescriptorList: { keyShapeInvalidated?: boolean; keyOriginType?: number; keyOriginResolution?: number; keyOriginRRectRadii?: { unitValueQuadVersion: number; topRight: DescriptorUnitsValue; topLeft: DescriptorUnitsValue; bottomLeft: DescriptorUnitsValue; bottomRight: DescriptorUnitsValue; }; keyOriginShapeBBox?: { unitValueQuadVersion: number; 'Top ': DescriptorUnitsValue | number; Left: DescriptorUnitsValue | number; Btom: DescriptorUnitsValue | number; Rght: DescriptorUnitsValue | number; }; keyOriginBoxCorners?: { rectangleCornerA: { Hrzn: number; Vrtc: number; }; rectangleCornerB: { Hrzn: number; Vrtc: number; }; rectangleCornerC: { Hrzn: number; Vrtc: number; }; rectangleCornerD: { Hrzn: number; Vrtc: number; }; }; Trnf?: { xx: number; xy: number; yx: number; yy: number; tx: number; ty: number; }, keyOriginIndex: number; }[]; } addHandler( 'vogk', hasKey('vectorOrigination'), (reader, target, left) => { if (readInt32(reader) !== 1) throw new Error(`Invalid vogk version`); const desc = readVersionAndDescriptor(reader) as VogkDescriptor; // console.log(require('util').inspect(desc, false, 99, true)); target.vectorOrigination = { keyDescriptorList: [] }; for (const i of desc.keyDescriptorList) { const item: KeyDescriptorItem = {}; if (i.keyShapeInvalidated != null) item.keyShapeInvalidated = i.keyShapeInvalidated; if (i.keyOriginType != null) item.keyOriginType = i.keyOriginType; if (i.keyOriginResolution != null) item.keyOriginResolution = i.keyOriginResolution; if (i.keyOriginShapeBBox) { item.keyOriginShapeBoundingBox = { top: parseUnitsOrNumber(i.keyOriginShapeBBox['Top ']), left: parseUnitsOrNumber(i.keyOriginShapeBBox.Left), bottom: parseUnitsOrNumber(i.keyOriginShapeBBox.Btom), right: parseUnitsOrNumber(i.keyOriginShapeBBox.Rght), }; } const rectRadii = i.keyOriginRRectRadii; if (rectRadii) { item.keyOriginRRectRadii = { topRight: parseUnits(rectRadii.topRight), topLeft: parseUnits(rectRadii.topLeft), bottomLeft: parseUnits(rectRadii.bottomLeft), bottomRight: parseUnits(rectRadii.bottomRight), }; } const corners = i.keyOriginBoxCorners; if (corners) { item.keyOriginBoxCorners = [ { x: corners.rectangleCornerA.Hrzn, y: corners.rectangleCornerA.Vrtc }, { x: corners.rectangleCornerB.Hrzn, y: corners.rectangleCornerB.Vrtc }, { x: corners.rectangleCornerC.Hrzn, y: corners.rectangleCornerC.Vrtc }, { x: corners.rectangleCornerD.Hrzn, y: corners.rectangleCornerD.Vrtc }, ]; } const trnf = i.Trnf; if (trnf) { item.transform = [trnf.xx, trnf.xy, trnf.yx, trnf.yy, trnf.tx, trnf.ty]; } target.vectorOrigination.keyDescriptorList.push(item); } skipBytes(reader, left()); }, (writer, target) => { target; const orig = target.vectorOrigination!; const desc: VogkDescriptor = { keyDescriptorList: [] }; for (let i = 0; i < orig.keyDescriptorList.length; i++) { const item = orig.keyDescriptorList[i]; desc.keyDescriptorList.push({} as any); // we're adding keyOriginIndex at the end const out = desc.keyDescriptorList[desc.keyDescriptorList.length - 1]; if (item.keyOriginType != null) out.keyOriginType = item.keyOriginType; if (item.keyOriginResolution != null) out.keyOriginResolution = item.keyOriginResolution; const radii = item.keyOriginRRectRadii; if (radii) { out.keyOriginRRectRadii = { unitValueQuadVersion: 1, topRight: unitsValue(radii.topRight, 'topRight'), topLeft: unitsValue(radii.topLeft, 'topLeft'), bottomLeft: unitsValue(radii.bottomLeft, 'bottomLeft'), bottomRight: unitsValue(radii.bottomRight, 'bottomRight'), }; } const box = item.keyOriginShapeBoundingBox; if (box) { out.keyOriginShapeBBox = { unitValueQuadVersion: 1, 'Top ': unitsValue(box.top, 'top'), Left: unitsValue(box.left, 'left'), Btom: unitsValue(box.bottom, 'bottom'), Rght: unitsValue(box.right, 'right'), }; } const corners = item.keyOriginBoxCorners; if (corners && corners.length === 4) { out.keyOriginBoxCorners = { rectangleCornerA: { Hrzn: corners[0].x, Vrtc: corners[0].y }, rectangleCornerB: { Hrzn: corners[1].x, Vrtc: corners[1].y }, rectangleCornerC: { Hrzn: corners[2].x, Vrtc: corners[2].y }, rectangleCornerD: { Hrzn: corners[3].x, Vrtc: corners[3].y }, }; } const transform = item.transform; if (transform && transform.length === 6) { out.Trnf = { xx: transform[0], xy: transform[1], yx: transform[2], yy: transform[3], tx: transform[4], ty: transform[5], }; } if (item.keyShapeInvalidated != null) out.keyShapeInvalidated = item.keyShapeInvalidated; out.keyOriginIndex = i; } writeInt32(writer, 1); // version writeVersionAndDescriptor(writer, '', 'null', desc); } ); addHandler( 'lmfx', target => target.effects !== undefined && hasMultiEffects(target.effects), (reader, target, left) => { const version = readUint32(reader); if (version !== 0) throw new Error('Invalid lmfx version'); const desc: LmfxDescriptor = readVersionAndDescriptor(reader); // console.log('READ', require('util').inspect(desc, false, 99, true)); // discard if read in 'lrFX' or 'lfx2' section target.effects = parseEffects(desc, !!reader.logMissingFeatures); skipBytes(reader, left()); }, (writer, target, _, options) => { const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true); // console.log('WRITE', require('util').inspect(desc, false, 99, true)); writeUint32(writer, 0); // version writeVersionAndDescriptor(writer, '', 'null', desc); }, ); addHandler( 'lrFX', hasKey('effects'), (reader, target, left) => { if (!target.effects) target.effects = readEffects(reader); skipBytes(reader, left()); }, (writer, target) => { writeEffects(writer, target.effects!); }, ); addHandler( 'luni', hasKey('name'), (reader, target, left) => { if (left() > 4) { const length = readUint32(reader); if (left() >= (length * 2)) { target.name = readUnicodeStringWithLength(reader, length); } else { if (reader.logDevFeatures) reader.log('name in luni section is too long'); } } else { if (reader.logDevFeatures) reader.log('empty luni section'); } skipBytes(reader, left()); }, (writer, target) => { writeUnicodeString(writer, target.name!); // writeUint16(writer, 0); // padding (but not extending string length) }, ); addHandler( 'lnsr', hasKey('nameSource'), (reader, target) => target.nameSource = readSignature(reader), (writer, target) => writeSignature(writer, target.nameSource!), ); addHandler( 'lyid', hasKey('id'), (reader, target) => { target.id = readUint32(reader); }, (writer, target, _psd, options) => { let id = target.id!; while (options.layerIds.has(id)) id += 100; // make sure we don't have duplicate layer ids writeUint32(writer, id); options.layerIds.add(id); options.layerToId.set(target, id); }, ); addHandler( 'lsct', hasKey('sectionDivider'), (reader, target, left) => { target.sectionDivider = { type: readUint32(reader) }; if (left()) { checkSignature(reader, '8BIM'); target.sectionDivider.key = readSignature(reader); } if (left()) { target.sectionDivider.subType = readUint32(reader); } }, (writer, target) => { writeUint32(writer, target.sectionDivider!.type); if (target.sectionDivider!.key) { writeSignature(writer, '8BIM'); writeSignature(writer, target.sectionDivider!.key); if (target.sectionDivider!.subType !== undefined) { writeUint32(writer, target.sectionDivider!.subType); } } }, ); // it seems lsdk is used when there's a layer is nested more than 6 levels, but I don't know why? // maybe some limitation of old version of PS? addHandlerAlias('lsdk', 'lsct'); addHandler( 'clbl', hasKey('blendClippendElements'), (reader, target) => { target.blendClippendElements = !!readUint8(reader); skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.blendClippendElements ? 1 : 0); writeZeros(writer, 3); }, ); addHandler( 'infx', hasKey('blendInteriorElements'), (reader, target) => { target.blendInteriorElements = !!readUint8(reader); skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.blendInteriorElements ? 1 : 0); writeZeros(writer, 3); }, ); addHandler( 'knko', hasKey('knockout'), (reader, target) => { target.knockout = !!readUint8(reader); skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.knockout ? 1 : 0); writeZeros(writer, 3); }, ); addHandler( 'lmgm', hasKey('layerMaskAsGlobalMask'), (reader, target) => { target.layerMaskAsGlobalMask = !!readUint8(reader); skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.layerMaskAsGlobalMask ? 1 : 0); writeZeros(writer, 3); }, ); addHandler( 'lspf', hasKey('protected'), (reader, target) => { const flags = readUint32(reader); target.protected = { transparency: (flags & 0x01) !== 0, composite: (flags & 0x02) !== 0, position: (flags & 0x04) !== 0, }; if (flags & 0x08) target.protected.artboards = true; }, (writer, target) => { const flags = (target.protected!.transparency ? 0x01 : 0) | (target.protected!.composite ? 0x02 : 0) | (target.protected!.position ? 0x04 : 0) | (target.protected!.artboards ? 0x08 : 0); writeUint32(writer, flags); }, ); addHandler( 'lclr', hasKey('layerColor'), (reader, target) => { const color = readUint16(reader); skipBytes(reader, 6); target.layerColor = layerColors[color]; }, (writer, target) => { const index = layerColors.indexOf(target.layerColor!); writeUint16(writer, index === -1 ? 0 : index); writeZeros(writer, 6); }, ); interface CustomDescriptor { layerTime?: number; } interface CmlsDescriptor { origFXRefPoint?: { Hrzn: number; Vrtc: number; }; LyrI: number; layerSettings: { enab?: boolean; Ofst?: { Hrzn: number; Vrtc: number; }; FXRefPoint?: { Hrzn: number; Vrtc: number; }; compList: number[]; }[]; } addHandler( 'shmd', // Metadata setting target => target.timestamp !== undefined || target.animationFrames !== undefined || target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined, (reader, target, left) => { const count = readUint32(reader); for (let i = 0; i < count; i++) { checkSignature(reader, '8BIM'); const key = readSignature(reader); readUint8(reader); // copy skipBytes(reader, 3); readSection(reader, 1, left => { if (key === 'cust') { const desc = readVersionAndDescriptor(reader) as CustomDescriptor; // console.log('cust', target.name, require('util').inspect(desc, false, 99, true)); if (desc.layerTime !== undefined) target.timestamp = desc.layerTime; } else if (key === 'mlst') { const desc = readVersionAndDescriptor(reader) as FrameListDescriptor; // console.log('mlst', target.name, require('util').inspect(desc, false, 99, true)); target.animationFrames = []; for (let i = 0; i < desc.LaSt.length; i++) { const f = desc.LaSt[i]; const frame: AnimationFrame = { frames: f.FrLs }; if (f.enab !== undefined) frame.enable = f.enab; if (f.Ofst) frame.offset = horzVrtcToXY(f.Ofst); if (f.FXRf) frame.referencePoint = horzVrtcToXY(f.FXRf); if (f.Lefx) frame.effects = parseEffects(f.Lefx, !!reader.logMissingFeatures); if (f.blendOptions && f.blendOptions.Opct) frame.opacity = parsePercent(f.blendOptions.Opct); target.animationFrames.push(frame); } } else if (key === 'mdyn') { // frame flags readUint16(reader); // unknown const propagate = readUint8(reader); const flags = readUint8(reader); target.animationFrameFlags = { propagateFrameOne: !propagate, unifyLayerPosition: (flags & 1) !== 0, unifyLayerStyle: (flags & 2) !== 0, unifyLayerVisibility: (flags & 4) !== 0, }; } else if (key === 'tmln') { const desc = readVersionAndDescriptor(reader) as TimelineDescriptor; const timeScope = desc.timeScope; // console.log('tmln', target.name, target.id, require('util').inspect(desc, false, 99, true)); const timeline: Timeline = { start: frac(timeScope.Strt), duration: frac(timeScope.duration), inTime: frac(timeScope.inTime), outTime: frac(timeScope.outTime), autoScope: desc.autoScope, audioLevel: desc.audioLevel, }; if (desc.trackList) { timeline.tracks = parseTrackList(desc.trackList, !!reader.logMissingFeatures); } target.timeline = timeline; // console.log('tmln:result', target.name, target.id, require('util').inspect(timeline, false, 99, true)); } else if (key === 'cmls') { const desc = readVersionAndDescriptor(reader) as CmlsDescriptor; // console.log('cmls', require('util').inspect(desc, false, 99, true)); target.comps = { settings: [], }; if (desc.origFXRefPoint) target.comps.originalEffectsReferencePoint = { x: desc.origFXRefPoint.Hrzn, y: desc.origFXRefPoint.Vrtc }; for (const item of desc.layerSettings) { target.comps.settings.push({ compList: item.compList }); const t = target.comps.settings[target.comps.settings.length - 1]; if ('enab' in item) t.enabled = item.enab; if (item.Ofst) t.offset = { x: item.Ofst.Hrzn, y: item.Ofst.Vrtc }; if (item.FXRefPoint) t.effectsReferencePoint = { x: item.FXRefPoint.Hrzn, y: item.FXRefPoint.Vrtc }; } } else if (key === 'extn') { interface ExtnDescriptor { generatorSettings: { exportAs: { exportOption: string; }; layerTime: number; }; } const desc = readVersionAndDescriptor(reader) as ExtnDescriptor; // console.log(require('util').inspect(desc, false, 99, true)); desc; // TODO: save this reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key); } else { reader.logMissingFeatures && reader.log('Unhandled "shmd" section key', key); } skipBytes(reader, left()); }); } skipBytes(reader, left()); }, (writer, target, _, options) => { const { animationFrames, animationFrameFlags, timestamp, timeline, comps } = target; let count = 0; if (animationFrames) count++; if (animationFrameFlags) count++; if (timeline) count++; if (timestamp !== undefined) count++; if (comps) count++; writeUint32(writer, count); if (animationFrames) { writeSignature(writer, '8BIM'); writeSignature(writer, 'mlst'); writeUint8(writer, 0); // copy (always false) writeZeros(writer, 3); writeSection(writer, 2, () => { const desc: FrameListDescriptor = { LaID: target.id ?? 0, LaSt: [], }; for (let i = 0; i < animationFrames.length; i++) { const f = animationFrames[i]; const frame: FrameDescriptor = {} as any; if (f.enable !== undefined) frame.enab = f.enable; frame.FrLs = f.frames; if (f.offset) frame.Ofst = xyToHorzVrtc(f.offset); if (f.referencePoint) frame.FXRf = xyToHorzVrtc(f.referencePoint); if (f.effects) frame.Lefx = serializeEffects(f.effects, false, false); if (f.opacity !== undefined) frame.blendOptions = { Opct: unitsPercent(f.opacity) }; desc.LaSt.push(frame); } writeVersionAndDescriptor(writer, '', 'null', desc); }, true); } if (animationFrameFlags) { writeSignature(writer, '8BIM'); writeSignature(writer, 'mdyn'); writeUint8(writer, 0); // copy (always false) writeZeros(writer, 3); writeSection(writer, 2, () => { writeUint16(writer, 0); // unknown writeUint8(writer, animationFrameFlags.propagateFrameOne ? 0x0 : 0xf); writeUint8(writer, (animationFrameFlags.unifyLayerPosition ? 1 : 0) | (animationFrameFlags.unifyLayerStyle ? 2 : 0) | (animationFrameFlags.unifyLayerVisibility ? 4 : 0)); }); } if (timeline) { writeSignature(writer, '8BIM'); writeSignature(writer, 'tmln'); writeUint8(writer, 0); // copy (always false) writeZeros(writer, 3); writeSection(writer, 2, () => { const desc: TimelineDescriptor = { Vrsn: 1, timeScope: { Vrsn: 1, Strt: timeline.start, duration: timeline.duration, inTime: timeline.inTime, outTime: timeline.outTime, }, autoScope: timeline.autoScope, audioLevel: timeline.audioLevel, } as any; if (timeline.tracks) { desc.trackList = serializeTrackList(timeline.tracks); } const id = options.layerToId.get(target) || target.id; if (!id) throw new Error('You need to provide layer.id value whan writing document with animations'); desc.LyrI = id; // console.log('WRITE:tmln', target.name, target.id, require('util').inspect(desc, false, 99, true)); writeVersionAndDescriptor(writer, '', 'null', desc, 'anim'); }, true); } if (timestamp !== undefined) { writeSignature(writer, '8BIM'); writeSignature(writer, 'cust'); writeUint8(writer, 0); // copy (always false) writeZeros(writer, 3); writeSection(writer, 2, () => { const desc: CustomDescriptor = { layerTime: timestamp, }; writeVersionAndDescriptor(writer, '', 'metadata', desc); }, true); } if (comps) { writeSignature(writer, '8BIM'); writeSignature(writer, 'cmls'); writeUint8(writer, 0); // copy (always false) writeZeros(writer, 3); writeSection(writer, 2, () => { const id = options.layerToId.get(target) || target.id; if (!id) throw new Error('You need to provide layer.id value whan writing document with layer comps'); const desc: CmlsDescriptor = {} as any; if (comps.originalEffectsReferencePoint) { desc.origFXRefPoint = { Hrzn: comps.originalEffectsReferencePoint.x, Vrtc: comps.originalEffectsReferencePoint.y }; } desc.LyrI = id; desc.layerSettings = []; for (const item of comps.settings) { const t: CmlsDescriptor['layerSettings'][0] = {} as any; if (item.enabled !== undefined) t.enab = item.enabled; if (item.offset) t.Ofst = { Hrzn: item.offset.x, Vrtc: item.offset.y }; if (item.effectsReferencePoint) t.FXRefPoint = { Hrzn: item.effectsReferencePoint.x, Vrtc: item.effectsReferencePoint.y }; t.compList = item.compList; desc.layerSettings.push(t); } // console.log('cmls', require('util').inspect(desc, false, 99, true)); writeVersionAndDescriptor(writer, '', 'null', desc); }, true); } }, ); addHandler( 'PxSc', () => false, (reader, target) => { const desc = readVersionAndDescriptor(reader, true) as PxScDescriptor; // console.log('PxSc', require('util').inspect(desc, false, 99, true)); if (desc.pixelSourceType === 1986285651) { target.pixelSource = { type: 'vdPS', origin: { x: desc.origin.Hrzn, y: desc.origin.Vrtc }, interpretation: { interpretAlpha: desc.interpretation.interpretAlpha.split('.')[1] as any, profile: desc.interpretation.profile, }, frameReader: { type: 'QTFR', link: { name: desc.frameReader['Lnk ']['Nm '], fullPath: desc.frameReader['Lnk '].fullPath, originalPath: desc.frameReader['Lnk '].originalPath, relativePath: desc.frameReader['Lnk '].relPath, alias: desc.frameReader['Lnk '].alis, }, mediaDescriptor: desc.frameReader.mediaDescriptor, }, showAlteredVideo: desc.showAlteredVideo, }; } else { reader.log(`Unknown pixelSourceType`); } }, (writer, target) => { const source = target.pixelSource!; const desc: PxScDescriptor = { _name: '', _classID: 'PixelSource', pixelSourceType: 1986285651, // vdP descVersion: 1, origin: { Hrzn: source.origin.x, Vrtc: source.origin.y }, interpretation: { _name: '', _classID: 'footageInterpretation', Vrsn: 1, interpretAlpha: `alphaInterpretation.${source.interpretation.interpretAlpha}` as any, profile: source.interpretation.profile, }, frameReader: { _name: '', _classID: 'FrameReader', frameReaderType: 1364477522, // QTF descVersion: 1, 'Lnk ': { _name: '', _classID: 'ExternalFileLink', descVersion: 2, 'Nm ': source.frameReader.link.name, fullPath: source.frameReader.link.fullPath, originalPath: source.frameReader.link.originalPath, alis: source.frameReader.link.alias, relPath: source.frameReader.link.relativePath, }, mediaDescriptor: source.frameReader.mediaDescriptor, }, showAlteredVideo: source.showAlteredVideo, }; writeVersionAndDescriptor(writer, '', 'PixelSource', desc); }, ); addHandler( 'vstk', hasKey('vectorStroke'), (reader, target, left) => { const desc = readVersionAndDescriptor(reader) as StrokeDescriptor; // console.log(require('util').inspect(desc, false, 99, true)); target.vectorStroke = { strokeEnabled: desc.strokeEnabled, fillEnabled: desc.fillEnabled, lineWidth: parseUnits(desc.strokeStyleLineWidth), lineDashOffset: parseUnits(desc.strokeStyleLineDashOffset), miterLimit: desc.strokeStyleMiterLimit, lineCapType: strokeStyleLineCapType.decode(desc.strokeStyleLineCapType), lineJoinType: strokeStyleLineJoinType.decode(desc.strokeStyleLineJoinType), lineAlignment: strokeStyleLineAlignment.decode(desc.strokeStyleLineAlignment), scaleLock: desc.strokeStyleScaleLock, strokeAdjust: desc.strokeStyleStrokeAdjust, lineDashSet: desc.strokeStyleLineDashSet.map(parseUnits), blendMode: BlnM.decode(desc.strokeStyleBlendMode), opacity: parsePercent(desc.strokeStyleOpacity), content: parseVectorContent(desc.strokeStyleContent), resolution: desc.strokeStyleResolution, }; skipBytes(reader, left()); }, (writer, target) => { const stroke = target.vectorStroke!; const desc: StrokeDescriptor = { strokeStyleVersion: 2, strokeEnabled: !!stroke.strokeEnabled, fillEnabled: !!stroke.fillEnabled, strokeStyleLineWidth: stroke.lineWidth || { value: 3, units: 'Points' }, strokeStyleLineDashOffset: stroke.lineDashOffset || { value: 0, units: 'Points' }, strokeStyleMiterLimit: stroke.miterLimit ?? 100, strokeStyleLineCapType: strokeStyleLineCapType.encode(stroke.lineCapType), strokeStyleLineJoinType: strokeStyleLineJoinType.encode(stroke.lineJoinType), strokeStyleLineAlignment: strokeStyleLineAlignment.encode(stroke.lineAlignment), strokeStyleScaleLock: !!stroke.scaleLock, strokeStyleStrokeAdjust: !!stroke.strokeAdjust, strokeStyleLineDashSet: stroke.lineDashSet || [], strokeStyleBlendMode: BlnM.encode(stroke.blendMode), strokeStyleOpacity: unitsPercent(stroke.opacity ?? 1), strokeStyleContent: serializeVectorContent( stroke.content || { type: 'color', color: { r: 0, g: 0, b: 0 } }).descriptor, strokeStyleResolution: stroke.resolution ?? 72, }; writeVersionAndDescriptor(writer, '', 'strokeStyle', desc); }, ); interface ArtbDescriptor { artboardRect: { 'Top ': number; Left: number; Btom: number; Rght: number; }; guideIndeces: any[]; artboardPresetName: string; 'Clr ': DescriptorColor; artboardBackgroundType: number; } addHandler( 'artb', // per-layer arboard info hasKey('artboard'), (reader, target, left) => { const desc = readVersionAndDescriptor(reader) as ArtbDescriptor; const rect = desc.artboardRect; target.artboard = { rect: { top: rect['Top '], left: rect.Left, bottom: rect.Btom, right: rect.Rght }, guideIndices: desc.guideIndeces, presetName: desc.artboardPresetName, color: parseColor(desc['Clr ']), backgroundType: desc.artboardBackgroundType, }; skipBytes(reader, left()); }, (writer, target) => { const artboard = target.artboard!; const rect = artboard.rect; const desc: ArtbDescriptor = { artboardRect: { 'Top ': rect.top, Left: rect.left, Btom: rect.bottom, Rght: rect.right }, guideIndeces: artboard.guideIndices || [], artboardPresetName: artboard.presetName || '', 'Clr ': serializeColor(artboard.color), artboardBackgroundType: artboard.backgroundType ?? 1, }; writeVersionAndDescriptor(writer, '', 'artboard', desc); }, ); addHandler( 'sn2P', hasKey('usingAlignedRendering'), (reader, target) => target.usingAlignedRendering = !!readUint32(reader), (writer, target) => writeUint32(writer, target.usingAlignedRendering ? 1 : 0), ); const placedLayerTypes: PlacedLayerType[] = ['unknown', 'vector', 'raster', 'image stack']; function parseWarp(warp: WarpDescriptor & QuiltWarpDescriptor): Warp { const result: Warp = { style: warpStyle.decode(warp.warpStyle), ...(warp.warpValues ? { values: warp.warpValues } : { value: warp.warpValue || 0 }), perspective: warp.warpPerspective || 0, perspectiveOther: warp.warpPerspectiveOther || 0, rotate: Ornt.decode(warp.warpRotate), bounds: warp.bounds && { top: parseUnitsOrNumber(warp.bounds['Top ']), left: parseUnitsOrNumber(warp.bounds.Left), bottom: parseUnitsOrNumber(warp.bounds.Btom), right: parseUnitsOrNumber(warp.bounds.Rght), }, uOrder: warp.uOrder, vOrder: warp.vOrder, }; if (warp.deformNumRows != null || warp.deformNumCols != null) { result.deformNumRows = warp.deformNumRows; result.deformNumCols = warp.deformNumCols; } const envelopeWarp = warp.customEnvelopeWarp; if (envelopeWarp) { result.customEnvelopeWarp = { meshPoints: [], }; const xs = envelopeWarp.meshPoints.find(i => i.type === 'Hrzn')?.values || []; const ys = envelopeWarp.meshPoints.find(i => i.type === 'Vrtc')?.values || []; for (let i = 0; i < xs.length; i++) { result.customEnvelopeWarp!.meshPoints.push({ x: xs[i], y: ys[i] }); } if (envelopeWarp.quiltSliceX || envelopeWarp.quiltSliceY) { result.customEnvelopeWarp.quiltSliceX = envelopeWarp.quiltSliceX?.[0]?.values || []; result.customEnvelopeWarp.quiltSliceY = envelopeWarp.quiltSliceY?.[0]?.values || []; } } return result; } function isQuiltWarp(warp: Warp) { return warp.deformNumCols != null || warp.deformNumRows != null || warp.customEnvelopeWarp?.quiltSliceX || warp.customEnvelopeWarp?.quiltSliceY; } function encodeWarp(warp: Warp): WarpDescriptor { const bounds = warp.bounds; const desc: WarpDescriptor = { warpStyle: warpStyle.encode(warp.style), ...(warp.values ? { warpValues: warp.values } : { warpValue: warp.value || 0 }), warpPerspective: warp.perspective || 0, warpPerspectiveOther: warp.perspectiveOther || 0, warpRotate: Ornt.encode(warp.rotate), bounds: /*1 ? { // testing _classID: 'classFloatRect', 'Top ': bounds && bounds.top && bounds.top.value || 0, Left: bounds && bounds.left && bounds.left.value || 0, Btom: bounds && bounds.bottom && bounds.bottom.value || 0, Rght: bounds && bounds.right && bounds.right.value || 0, } :*/ { 'Top ': unitsValue(bounds && bounds.top || { units: 'Pixels', value: 0 }, 'bounds.top'), Left: unitsValue(bounds && bounds.left || { units: 'Pixels', value: 0 }, 'bounds.left'), Btom: unitsValue(bounds && bounds.bottom || { units: 'Pixels', value: 0 }, 'bounds.bottom'), Rght: unitsValue(bounds && bounds.right || { units: 'Pixels', value: 0 }, 'bounds.right'), }, uOrder: warp.uOrder || 0, vOrder: warp.vOrder || 0, }; const isQuilt = isQuiltWarp(warp); if (isQuilt) { const desc2 = desc as QuiltWarpDescriptor; desc2.deformNumRows = warp.deformNumRows || 0; desc2.deformNumCols = warp.deformNumCols || 0; } const customEnvelopeWarp = warp.customEnvelopeWarp; if (customEnvelopeWarp) { const meshPoints = customEnvelopeWarp.meshPoints || []; if (isQuilt) { const desc2 = desc as QuiltWarpDescriptor; desc2.customEnvelopeWarp = { _name: '', _classID: 'customEnvelopeWarp', quiltSliceX: [{ type: 'quiltSliceX', values: customEnvelopeWarp.quiltSliceX || [], }], quiltSliceY: [{ type: 'quiltSliceY', values: customEnvelopeWarp.quiltSliceY || [], }], meshPoints: [ { type: 'Hrzn', values: meshPoints.map(p => p.x) }, { type: 'Vrtc', values: meshPoints.map(p => p.y) }, ], }; } else { desc.customEnvelopeWarp = { _name: '', _classID: 'customEnvelopeWarp', meshPoints: [ { type: 'Hrzn', values: meshPoints.map(p => p.x) }, { type: 'Vrtc', values: meshPoints.map(p => p.y) }, ], }; } } return desc; } addHandler( 'PlLd', hasKey('placedLayer'), (reader, target, left) => { if (readSignature(reader) !== 'plcL') throw new Error(`Invalid PlLd signature`); if (readInt32(reader) !== 3) throw new Error(`Invalid PlLd version`); const id = readPascalString(reader, 1); const pageNumber = readInt32(reader); const totalPages = readInt32(reader); // TODO: check how this works ? readInt32(reader); // anitAliasPolicy 16 const placedLayerType = readInt32(reader); // 0 = unknown, 1 = vector, 2 = raster, 3 = image stack if (!placedLayerTypes[placedLayerType]) throw new Error('Invalid PlLd type'); const transform: number[] = []; for (let i = 0; i < 8; i++) transform.push(readFloat64(reader)); // x, y of 4 corners of the transform const warpVersion = readInt32(reader); if (warpVersion !== 0) throw new Error(`Invalid Warp version ${warpVersion}`); const warp: WarpDescriptor & QuiltWarpDescriptor = readVersionAndDescriptor(reader); target.placedLayer = target.placedLayer || { // skip if SoLd already set it id, type: placedLayerTypes[placedLayerType], pageNumber, totalPages, transform, warp: parseWarp(warp), }; // console.log('PlLd warp', require('util').inspect(warp, false, 99, true)); // console.log('PlLd', require('util').inspect(target.placedLayer, false, 99, true)); skipBytes(reader, left()); }, (writer, target) => { const placed = target.placedLayer!; writeSignature(writer, 'plcL'); writeInt32(writer, 3); // version if (!placed.id || typeof placed.id !== 'string' || !/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(placed.id)) { throw new Error('Placed layer ID must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4)'); } writePascalString(writer, placed.id, 1); writeInt32(writer, 1); // pageNumber writeInt32(writer, 1); // totalPages writeInt32(writer, 16); // anitAliasPolicy if (placedLayerTypes.indexOf(placed.type) === -1) throw new Error('Invalid placedLayer type'); writeInt32(writer, placedLayerTypes.indexOf(placed.type)); for (let i = 0; i < 8; i++) writeFloat64(writer, placed.transform[i]); writeInt32(writer, 0); // warp version const warp = getWarpFromPlacedLayer(placed); const isQuilt = isQuiltWarp(warp); const type = isQuilt ? 'quiltWarp' : 'warp'; writeVersionAndDescriptor(writer, '', type, encodeWarp(warp), type); }, ); interface HrznVrtcDescriptor { _name: ''; _classID: 'Pnt '; Hrzn: DescriptorUnitsValue; Vrtc: DescriptorUnitsValue; } /* interface K3DLight { 'Nm ': string; 'Rd ': number; 'Grn ': number; 'Bl ': number; hots: number; FlOf: number; shdw: number; attn: boolean; attt: number; atta: number; attb: number; attc: number; orad: number; irad: number; mult: number; Type: number; ison: boolean; ssml: number; afon: boolean; afpw: number; key3DMatrix: { key3DMatrixData: Uint8Array; }, 'X ': number; 'Y ': number; 'Z ': number; tarx: number; tary: number; tarz: number; key3DPosition: { key3DXPos: number; key3DYPos: number; key3DZPos: number; key3DXAngle: number; key3DYAngle: number; key3DZAngle: number; }; } */ type SoLdDescriptorFilterItem = { _name: '', _classID: 'filterFX', 'Nm ': string; blendOptions: { _name: ''; _classID: 'blendOptions'; Opct: DescriptorUnitsValue; 'Md ': string; // blend mode }; enab: boolean; hasoptions: boolean; FrgC: DescriptorColor; BckC: DescriptorColor; } & ({ filterID: 1098281575; // average } | { filterID: 1114403360; // blur } | { filterID: 1114403405; // blur more } | { filterID: 697; Fltr: { _name: 'Box Blur'; _classID: 'boxblur'; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 1198747202; Fltr: { _name: 'Gaussian Blur' | '高斯模糊'; _classID: 'GsnB'; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 1299476034; Fltr: { _name: 'Motion Blur'; _classID: 'MtnB'; Angl: number; Dstn: DescriptorUnitsValue; }; } | { filterID: 1382313026; Fltr: { _name: 'Radial Blur'; _classID: 'RdlB'; Amnt: number; BlrM: string; BlrQ: string; }; } | { filterID: 702; Fltr: { _name: 'Shape Blur'; _classID: 'shapeBlur'; 'Rds ': DescriptorUnitsValue; customShape: { _name: ''; _classID: 'customShape'; 'Nm ': string; Idnt: string; }; }; } | { filterID: 1399681602; Fltr: { _name: 'Smart Blur'; _classID: 'SmrB'; 'Rds ': number; Thsh: number; SmBQ: string; SmBM: string; }; } | { filterID: 701; Fltr: { _name: 'Surface Blur'; _classID: 'surfaceBlur'; 'Rds ': DescriptorUnitsValue; Thsh: number; }; } | { filterID: 1148416108; Fltr: { _name: 'Displace'; _classID: 'Dspl'; HrzS: number; VrtS: number; DspM: string; UndA: string; DspF: { sig: string; path: string; }; }; } | { filterID: 1349411688; Fltr: { _name: 'Pinch'; _classID: 'Pnch'; Amnt: number; }; } | { filterID: 1349284384; Fltr: { _name: 'Polar Coordinates'; _classID: 'Plr '; Cnvr: string; }; } | { filterID: 1383099493; Fltr: { _name: 'Ripple'; _classID: 'Rple'; Amnt: number; RplS: string; }; } | { filterID: 1399353888; Fltr: { _name: 'Shear'; _classID: 'Shr '; ShrP: { _name: '', _classID: 'Pnt ', Hrzn: number; Vrtc: number; }[]; UndA: string; ShrS: number; ShrE: number; }; } | { filterID: 1399875698; Fltr: { _name: 'Spherize'; _classID: 'Sphr'; Amnt: number; SphM: string; }; } | { filterID: 1417114220; Fltr: { _name: 'Twirl'; _classID: 'Twrl'; Angl: number; }; } | { filterID: 1466005093; Fltr: { _name: 'Wave'; _classID: 'Wave'; Wvtp: string; NmbG: number; WLMn: number; WLMx: number; AmMn: number; AmMx: number; SclH: number; SclV: number; UndA: string; RndS: number; }; } | { filterID: 1516722791; Fltr: { _name: 'ZigZag'; _classID: 'ZgZg'; Amnt: number; NmbR: number; ZZTy: string; }; } | { filterID: 1097092723; Fltr: { _name: 'Add Noise'; _classID: 'AdNs'; Dstr: string; Nose: DescriptorUnitsValue; Mnch: boolean; FlRs: number; }; } | { filterID: 1148416099; } | { filterID: 1148417107; Fltr: { _name: 'Dust & Scratches'; _classID: 'DstS'; 'Rds ': number; Thsh: number; }; } | { filterID: 1298427424; Fltr: { _name: 'Median'; _classID: 'Mdn '; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 633; Fltr: { _name: 'Reduce Noise'; _classID: 'denoise'; ClNs: DescriptorUnitsValue; // percent Shrp: DescriptorUnitsValue; // percent removeJPEGArtifact: boolean; channelDenoise: { _name: ''; _classID: 'channelDenoiseParams'; Chnl: string[]; Amnt: number; EdgF?: number; }[]; preset: string; }; } | { filterID: 1131180616; Fltr: { _name: 'Color Halftone'; _classID: 'ClrH'; 'Rds ': number; Ang1: number; Ang2: number; Ang3: number; Ang4: number; }; } | { filterID: 1131574132; Fltr: { _name: 'Crystallize'; _classID: 'Crst'; ClSz: number; FlRs: number; }; } | { filterID: 1180922912; } | { filterID: 1181902701; } | { filterID: 1299870830; Fltr: { _name: 'Mezzotint'; _classID: 'Mztn'; MztT: string; FlRs: number; }; } | { filterID: 1299407648; Fltr: { _name: 'Mosaic'; _classID: 'Msc '; ClSz: DescriptorUnitsValue; }; } | { filterID: 1349416044; Fltr: { _name: 'Pointillize'; _classID: 'Pntl'; ClSz: number; FlRs: number; }; } | { filterID: 1131177075; Fltr: { _name: 'Clouds'; _classID: 'Clds'; FlRs: number; }; } | { filterID: 1147564611; Fltr: { _name: 'Difference Clouds', _classID: 'DfrC', FlRs: number; }; } | { filterID: 1180856947; Fltr: { _name: 'Fibers'; _classID: 'Fbrs'; Vrnc: number; Strg: number; RndS: number; }; } | { filterID: 1282306886; Fltr: { _name: 'Lens Flare'; _classID: 'LnsF'; Brgh: number; FlrC: { _name: ''; _classID: 'Pnt '; Hrzn: number; Vrtc: number; }; 'Lns ': string; }; } /*| { filterID: 587; Fltr: { k3DLights: K3DLight[]; key3DCurrentCameraPosition: { key3DXPos: number; key3DYPos: number; key3DZPos: number; key3DXAngle: number; key3DYAngle: number; key3DZAngle: number; }, Glos: number; Mtrl: number; Exps: number; AmbB: number; AmbC: DescriptorColor; BmpA: number; BmpC: string[]; Wdth: number; Hght: number; }; }*/ | { filterID: 1399353968 | 1399353925 | 1399353933; } | { filterID: 698; Fltr: { _name: 'Smart Sharpen'; _classID: 'smartSharpen'; Amnt: DescriptorUnitsValue; // % 'Rds ': DescriptorUnitsValue; Thsh: number; Angl: number; moreAccurate: boolean; blur: string; preset: string; sdwM: { _name: 'Parameters', _classID: 'adaptCorrectTones', Amnt: DescriptorUnitsValue; // % Wdth: DescriptorUnitsValue; // % 'Rds ': number; }; hglM: { _name: 'Parameters', _classID: 'adaptCorrectTones', Amnt: DescriptorUnitsValue; // % Wdth: DescriptorUnitsValue; // % 'Rds ': number; }; }; } | { filterID: 1433301837; Fltr: { _name: 'Unsharp Mask'; _classID: 'UnsM'; Amnt: DescriptorUnitsValue; // % 'Rds ': DescriptorUnitsValue; Thsh: number; }; } | { filterID: 1147564832; Fltr: { _name: 'Diffuse'; _classID: 'Dfs '; 'Md ': string; FlRs: number; }; } | { filterID: 1164796531; Fltr: { _name: 'Emboss'; _classID: 'Embs'; Angl: number; Hght: number; Amnt: number; }; } | { filterID: 1165522034; Fltr: { _name: 'Extrude'; _classID: 'Extr'; ExtS: number; ExtD: number; ExtF: boolean; ExtM: boolean; ExtT: string; ExtR: string; FlRs: number; }; } | { filterID: 1181639749 | 1399616122; } | { filterID: 1416393504; Fltr: { _name: 'Tiles'; _classID: 'Tls '; TlNm: number; TlOf: number; FlCl: string; FlRs: number; }; } | { filterID: 1416782659; Fltr: { _name: 'Trace Contour'; _classID: 'TrcC'; 'Lvl ': number; 'Edg ': string; }; } | { filterID: 1466852384; Fltr: { _name: 'Wind'; _classID: 'Wnd '; WndM: string; Drct: string; }; } | { filterID: 1148089458; Fltr: { _name: 'De-Interlace'; _classID: 'Dntr'; IntE: string; IntC: string; }; } | { filterID: 1314149187; } | { filterID: 1131639917; Fltr: { _name: 'Custom'; _classID: 'Cstm'; 'Scl ': number; Ofst: number; Mtrx: number[]; }; } | { filterID: 1214736464; Fltr: { _name: 'High Pass'; _classID: 'HghP'; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 1299737888; Fltr: { _name: 'Maximum'; _classID: 'Mxm '; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 1299082528; Fltr: { _name: 'Minimum'; _classID: 'Mnm '; 'Rds ': DescriptorUnitsValue; }; } | { filterID: 1332114292; Fltr: { _name: 'Offset'; _classID: 'Ofst'; Hrzn: number; Vrtc: number; 'Fl ': string; }; } | { filterID: 991 | 943; // TODO: why 2 different IDs? do we need to handle them separately? Fltr: { _name: 'Rigid Transform'; _classID: 'rigidTransform'; 'null': string[]; // [Ordn.Trgt] rigidType: boolean; puppetShapeList?: { _name: ''; _classID: 'puppetShape'; rigidType: boolean; VrsM: number; VrsN: number; originalVertexArray: Uint8Array; deformedVertexArray: Uint8Array; indexArray: Uint8Array; pinOffsets: number[]; posFinalPins: number[]; pinVertexIndices: number[]; PinP: number[]; PnRt: number[]; PnOv: boolean[]; PnDp: number[]; meshQuality: number; meshExpansion: number; meshRigidity: number; imageResolution: number; meshBoundaryPath: { _name: ''; _classID: 'pathClass'; pathComponents: { _name: ''; _classID: 'PaCm'; shapeOperation: string; // shapeOperation.xor SbpL: { _name: ''; _classID: 'Sbpl'; Clsp: boolean; 'Pts ': { _name: ''; _classID: 'Pthp'; Anch: HrznVrtcDescriptor; 'Fwd ': HrznVrtcDescriptor; 'Bwd ': HrznVrtcDescriptor; Smoo: boolean; }[]; }[]; }[]; }; selectedPin: number[]; }[]; PuX0: number; PuX1: number; PuX2: number; PuX3: number; PuY0: number; PuY1: number; PuY2: number; PuY3: number; } } | { filterID: 1348620396; Fltr: { _name: 'Oil Paint Plugin'; _classID: 'PbPl'; KnNm: string; GpuY: boolean; LIWy: boolean; FPth: string; // PNaa: string; // PTaa: number; // PFaa: number; // PNab: string; // PTab: number; // PFab: number; // ... }; } /*| { filterID: 1282294642; Fltr: { _name: 'Lens Correction', _classID: 'LnCr', LnAg: boolean; LnAc: boolean; LnAv: boolean; LnAs: boolean; LnIp: boolean; LnFo: number; LnPr: string; LnIa: number; LnI0: number; LnI1: number; LnI2: number; LnI3: number; LnRa: number; LnVp: number; LnHp: number; LnSi: number; LnFt: number; LnSb: number; LnSt: number; LnRc: number; LnGm: number; LnBy: number; LnNa: number; LnIh: number; LnIv: number; LnIs: DescriptorColor; LnNm: boolean; }; }*//* | { filterID: 2089; Fltr: { _name: 'Adaptive Wide Angle'; _classID: '22C3EEBF-A978-4ca9-91DF-E4F0CCEE5ACE'; actV: number; cnsD?: Uint8Array; prjM: string; regM: string; focL: number; PhyF: number; CrpF: number; imgS: number; imgX: number; imgY: number; }; }*//* | { filterID: 1195730531; Fltr: { _name: 'Filter Gallery'; _classID: 'GEfc'; } & ({ GEfk: string; Pncl: number; StrP: number; PprB: number; } | ...); }; }*/ | { filterID: 1215521360; Fltr: { _name: 'HSB/HSL', _classID: 'HsbP', Inpt: string; Otpt: string; }; } | { filterID: 1122; Fltr: { _name: 'Oil Paint'; _classID: 'oilPaint'; lightingOn: boolean; stylization: number; cleanliness: number; brushScale: number; microBrush: number; LghD: number; specularity: number; }; } | { filterID: 1282492025; Fltr: { _name: 'Liquify'; _classID: 'LqFy'; LqMe: Uint8Array; }; } | { filterID: 442; Fltr: { _name: 'Perspective Warp'; _classID: 'perspectiveWarpTransform'; quads: { indices: number[]; }[]; vertices: HrznVrtcDescriptor[]; warpedVertices: HrznVrtcDescriptor[]; }; } | { filterID: 1131574899; Fltr: { _name: 'Curves'; _classID: 'Crvs'; presetKind: string; // 'presetKindType.presetKindCustom'; Adjs?: { _name: ''; _classID: 'CrvA'; Chnl: string[]; // 'Chnl.Cmps' | 'Chnl.Rd ' | 'Chnl.Grn ' | 'Chnl.Bl ' 'Crv '?: FilterCurvesCurvePoint[]; Mpng?: number[]; }[]; }; } | { filterID: 1231976050; } | { filterID: 1114793795; Fltr: { _name: 'Brightness/Contrast'; _classID: 'BrgC'; Brgh: number; Cntr: number; useLegacy: boolean; }; }); interface FilterCurvesCurvePoint { _name: ''; _classID: 'Pnt '; Hrzn: 0; Vrtc: 0; Cnty?: boolean; } interface SoLdDescriptorFilter { _name: '', _classID: 'filterFXStyle', enab: boolean, validAtPosition: boolean, filterMaskEnable: boolean, filterMaskLinked: boolean, filterMaskExtendWithWhite: boolean, filterFXList: SoLdDescriptorFilterItem[]; } function uint8ToFloat32(array: Uint8Array) { return new Float32Array(array.buffer.slice(array.byteOffset), 0, array.byteLength / 4); } function uint8ToUint32(array: Uint8Array) { return new Uint32Array(array.buffer.slice(array.byteOffset), 0, array.byteLength / 4); } function toUint8(array: Uint32Array | Float32Array) { return new Uint8Array(array.buffer, array.byteOffset, array.byteLength); } function arrayToPoints(array: number[] | Uint32Array | Float32Array) { const points: { x: number; y: number }[] = []; for (let i = 0; i < array.length; i += 2) { points.push({ x: array[i], y: array[i + 1] }); } return points; } function pointsToArray(points: { x: number; y: number }[]) { const array: number[] = []; for (let i = 0; i < points.length; i++) { array.push(points[i].x, points[i].y); } return array; } function uint8ToPoints(array: Uint8Array) { return arrayToPoints(uint8ToFloat32(array)); } function hrznVrtcToPoint(desc: HrznVrtcDescriptor) { return { x: parseUnits(desc.Hrzn), y: parseUnits(desc.Vrtc), }; } function pointToHrznVrtc(point: { x: UnitsValue; y: UnitsValue; }): HrznVrtcDescriptor { return { _name: '', _classID: 'Pnt ', Hrzn: unitsValue(point.x, 'x'), Vrtc: unitsValue(point.y, 'y'), }; } function parseFilterFXItem(f: SoLdDescriptorFilterItem, options: ReadOptions): Filter | undefined { const base: Omit = { name: f['Nm '], opacity: parsePercent(f.blendOptions.Opct), blendMode: BlnM.decode(f.blendOptions['Md ']), enabled: f.enab, hasOptions: f.hasoptions, foregroundColor: parseColor(f.FrgC), backgroundColor: parseColor(f.BckC), }; if ('Fltr' in f) { switch (f.Fltr._classID) { case 'boxblur': return { ...base, type: 'box blur', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'GsnB': return { ...base, type: 'gaussian blur', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'MtnB': return { ...base, type: 'motion blur', filter: { angle: f.Fltr.Angl, distance: parseUnits(f.Fltr.Dstn), }, }; case 'RdlB': return { ...base, type: 'radial blur', filter: { amount: f.Fltr.Amnt, method: BlrM.decode(f.Fltr.BlrM), quality: BlrQ.decode(f.Fltr.BlrQ), }, }; case 'shapeBlur': return { ...base, type: 'shape blur', filter: { radius: parseUnits(f.Fltr['Rds ']), customShape: { name: f.Fltr.customShape['Nm '], id: f.Fltr.customShape.Idnt }, }, }; case 'SmrB': return { ...base, type: 'smart blur', filter: { radius: f.Fltr['Rds '], threshold: f.Fltr.Thsh, quality: SmBQ.decode(f.Fltr.SmBQ), mode: SmBM.decode(f.Fltr.SmBM), }, }; case 'surfaceBlur': return { ...base, type: 'surface blur', filter: { radius: parseUnits(f.Fltr['Rds ']), threshold: f.Fltr.Thsh, }, }; case 'Dspl': return { ...base, type: 'displace', filter: { horizontalScale: f.Fltr.HrzS, verticalScale: f.Fltr.VrtS, displacementMap: DspM.decode(f.Fltr.DspM), undefinedAreas: UndA.decode(f.Fltr.UndA), displacementFile: { signature: f.Fltr.DspF.sig, path: f.Fltr.DspF.path, // TODO: this is decoded incorrectly ??? }, }, }; case 'Pnch': return { ...base, type: 'pinch', filter: { amount: f.Fltr.Amnt, }, }; case 'Plr ': return { ...base, type: 'polar coordinates', filter: { conversion: Cnvr.decode(f.Fltr.Cnvr), }, }; case 'Rple': return { ...base, type: 'ripple', filter: { amount: f.Fltr.Amnt, size: RplS.decode(f.Fltr.RplS), }, }; case 'Shr ': return { ...base, type: 'shear', filter: { shearPoints: f.Fltr.ShrP.map(p => ({ x: p.Hrzn, y: p.Vrtc })), shearStart: f.Fltr.ShrS, shearEnd: f.Fltr.ShrE, undefinedAreas: UndA.decode(f.Fltr.UndA), }, }; case 'Sphr': return { ...base, type: 'spherize', filter: { amount: f.Fltr.Amnt, mode: SphM.decode(f.Fltr.SphM), }, }; case 'Twrl': return { ...base, type: 'twirl', filter: { angle: f.Fltr.Angl, }, }; case 'Wave': return { ...base, type: 'wave', filter: { numberOfGenerators: f.Fltr.NmbG, type: Wvtp.decode(f.Fltr.Wvtp), wavelength: { min: f.Fltr.WLMn, max: f.Fltr.WLMx }, amplitude: { min: f.Fltr.AmMn, max: f.Fltr.AmMx }, scale: { x: f.Fltr.SclH, y: f.Fltr.SclV }, randomSeed: f.Fltr.RndS, undefinedAreas: UndA.decode(f.Fltr.UndA), }, }; case 'ZgZg': return { ...base, type: 'zigzag', filter: { amount: f.Fltr.Amnt, ridges: f.Fltr.NmbR, style: ZZTy.decode(f.Fltr.ZZTy), }, }; case 'AdNs': return { ...base, type: 'add noise', filter: { amount: parsePercent(f.Fltr.Nose), distribution: Dstr.decode(f.Fltr.Dstr), monochromatic: f.Fltr.Mnch, randomSeed: f.Fltr.FlRs, }, }; case 'DstS': return { ...base, type: 'dust and scratches', filter: { radius: f.Fltr['Rds '], threshold: f.Fltr.Thsh, }, }; case 'Mdn ': return { ...base, type: 'median', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'denoise': return { ...base, type: 'reduce noise', filter: { preset: f.Fltr.preset, removeJpegArtifact: f.Fltr.removeJPEGArtifact, reduceColorNoise: parsePercent(f.Fltr.ClNs), sharpenDetails: parsePercent(f.Fltr.Shrp), channelDenoise: f.Fltr.channelDenoise.map(c => ({ channels: c.Chnl.map(Chnl.decode), amount: c.Amnt, ...(c.EdgF ? { preserveDetails: c.EdgF } : {}), })), }, }; case 'ClrH': return { ...base, type: 'color halftone', filter: { radius: f.Fltr['Rds '], angle1: f.Fltr.Ang1, angle2: f.Fltr.Ang2, angle3: f.Fltr.Ang3, angle4: f.Fltr.Ang4, }, }; case 'Crst': return { ...base, type: 'crystallize', filter: { cellSize: f.Fltr.ClSz, randomSeed: f.Fltr.FlRs, }, }; case 'Mztn': return { ...base, type: 'mezzotint', filter: { type: MztT.decode(f.Fltr.MztT), randomSeed: f.Fltr.FlRs, }, }; case 'Msc ': return { ...base, type: 'mosaic', filter: { cellSize: parseUnits(f.Fltr.ClSz), }, }; case 'Pntl': return { ...base, type: 'pointillize', filter: { cellSize: f.Fltr.ClSz, randomSeed: f.Fltr.FlRs, }, }; case 'Clds': return { ...base, type: 'clouds', filter: { randomSeed: f.Fltr.FlRs, }, }; case 'DfrC': return { ...base, type: 'difference clouds', filter: { randomSeed: f.Fltr.FlRs, }, }; case 'Fbrs': return { ...base, type: 'fibers', filter: { variance: f.Fltr.Vrnc, strength: f.Fltr.Strg, randomSeed: f.Fltr.RndS, }, }; case 'LnsF': return { ...base, type: 'lens flare', filter: { brightness: f.Fltr.Brgh, position: { x: f.Fltr.FlrC.Hrzn, y: f.Fltr.FlrC.Vrtc }, lensType: Lns.decode(f.Fltr['Lns ']), }, }; case 'smartSharpen': return { ...base, type: 'smart sharpen', filter: { amount: parsePercent(f.Fltr.Amnt), radius: parseUnits(f.Fltr['Rds ']), threshold: f.Fltr.Thsh, angle: f.Fltr.Angl, moreAccurate: f.Fltr.moreAccurate, blur: blurType.decode(f.Fltr.blur), preset: f.Fltr.preset, shadow: { fadeAmount: parsePercent(f.Fltr.sdwM.Amnt), tonalWidth: parsePercent(f.Fltr.sdwM.Wdth), radius: f.Fltr.sdwM['Rds '], }, highlight: { fadeAmount: parsePercent(f.Fltr.hglM.Amnt), tonalWidth: parsePercent(f.Fltr.hglM.Wdth), radius: f.Fltr.hglM['Rds '], }, }, }; case 'UnsM': return { ...base, type: 'unsharp mask', filter: { amount: parsePercent(f.Fltr.Amnt), radius: parseUnits(f.Fltr['Rds ']), threshold: f.Fltr.Thsh, }, }; case 'Dfs ': return { ...base, type: 'diffuse', filter: { mode: DfsM.decode(f.Fltr['Md ']), randomSeed: f.Fltr.FlRs, }, }; case 'Embs': return { ...base, type: 'emboss', filter: { angle: f.Fltr.Angl, height: f.Fltr.Hght, amount: f.Fltr.Amnt, }, }; case 'Extr': return { ...base, type: 'extrude', filter: { type: ExtT.decode(f.Fltr.ExtT), size: f.Fltr.ExtS, depth: f.Fltr.ExtD, depthMode: ExtR.decode(f.Fltr.ExtR), randomSeed: f.Fltr.FlRs, solidFrontFaces: f.Fltr.ExtF, maskIncompleteBlocks: f.Fltr.ExtM, }, }; case 'Tls ': return { ...base, type: 'tiles', filter: { numberOfTiles: f.Fltr.TlNm, maximumOffset: f.Fltr.TlOf, fillEmptyAreaWith: FlCl.decode(f.Fltr.FlCl), randomSeed: f.Fltr.FlRs, }, }; case 'TrcC': return { ...base, type: 'trace contour', filter: { level: f.Fltr['Lvl '], edge: CntE.decode(f.Fltr['Edg ']), }, }; case 'Wnd ': return { ...base, type: 'wind', filter: { method: WndM.decode(f.Fltr.WndM), direction: Drct.decode(f.Fltr.Drct), }, }; case 'Dntr': return { ...base, type: 'de-interlace', filter: { eliminate: IntE.decode(f.Fltr.IntE), newFieldsBy: IntC.decode(f.Fltr.IntC), }, }; case 'Cstm': return { ...base, type: 'custom', filter: { scale: f.Fltr['Scl '], offset: f.Fltr.Ofst, matrix: f.Fltr.Mtrx, }, }; case 'HghP': return { ...base, type: 'high pass', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'Mxm ': return { ...base, type: 'maximum', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'Mnm ': return { ...base, type: 'minimum', filter: { radius: parseUnits(f.Fltr['Rds ']), }, }; case 'Ofst': return { ...base, type: 'offset', filter: { horizontal: f.Fltr.Hrzn, vertical: f.Fltr.Vrtc, undefinedAreas: FlMd.decode(f.Fltr['Fl ']), }, }; case 'rigidTransform': return { ...base, type: 'puppet', filter: { rigidType: f.Fltr.rigidType, bounds: [ { x: f.Fltr.PuX0, y: f.Fltr.PuY0, }, { x: f.Fltr.PuX1, y: f.Fltr.PuY1, }, { x: f.Fltr.PuX2, y: f.Fltr.PuY2, }, { x: f.Fltr.PuX3, y: f.Fltr.PuY3, }, ], puppetShapeList: f.Fltr.puppetShapeList!.map(p => ({ rigidType: p.rigidType, // TODO: VrsM // TODO: VrsN originalVertexArray: uint8ToPoints(p.originalVertexArray), deformedVertexArray: uint8ToPoints(p.deformedVertexArray), indexArray: Array.from(uint8ToUint32(p.indexArray)), pinOffsets: arrayToPoints(p.pinOffsets), posFinalPins: arrayToPoints(p.posFinalPins), pinVertexIndices: p.pinVertexIndices, selectedPin: p.selectedPin, pinPosition: arrayToPoints(p.PinP), pinRotation: p.PnRt, pinOverlay: p.PnOv, pinDepth: p.PnDp, meshQuality: p.meshQuality, meshExpansion: p.meshExpansion, meshRigidity: p.meshRigidity, imageResolution: p.imageResolution, meshBoundaryPath: { pathComponents: p.meshBoundaryPath.pathComponents.map(c => ({ shapeOperation: c.shapeOperation.split('.')[1], paths: c.SbpL.map(t => ({ closed: t.Clsp, points: t['Pts '].map(pt => ({ anchor: hrznVrtcToPoint(pt.Anch), forward: hrznVrtcToPoint(pt['Fwd ']), backward: hrznVrtcToPoint(pt['Bwd ']), smooth: pt.Smoo, })), })), })), }, })), }, }; case 'PbPl': { const parameters: { name: string; value: number; }[] = []; const Flrt = f.Fltr as any; for (let i = 0; i < fromAtoZ.length; i++) { if (!Flrt[`PN${fromAtoZ[i]}a`]) break; for (let j = 0; j < fromAtoZ.length; j++) { if (!Flrt[`PN${fromAtoZ[i]}${fromAtoZ[j]}`]) break; parameters.push({ name: Flrt[`PN${fromAtoZ[i]}${fromAtoZ[j]}`], value: Flrt[`PF${fromAtoZ[i]}${fromAtoZ[j]}`] }); } } return { ...base, type: 'oil paint plugin', filter: { name: f.Fltr.KnNm, gpu: f.Fltr.GpuY, lighting: f.Fltr.LIWy, parameters, }, } } // case 2089: return { // ...base, // type: 'adaptive wide angle', // params: { // correction: prjM.decode(f.Fltr.prjM), // focalLength: f.Fltr.focL, // cropFactor: f.Fltr.CrpF, // imageScale: f.Fltr.imgS, // imageX: f.Fltr.imgX, // imageY: f.Fltr.imgY, // }, // }; case 'HsbP': return { ...base, type: 'hsb/hsl', filter: { inputMode: ClrS.decode(f.Fltr.Inpt) as any, rowOrder: ClrS.decode(f.Fltr.Otpt) as any, }, }; case 'oilPaint': return { ...base, type: 'oil paint', filter: { lightingOn: f.Fltr.lightingOn, stylization: f.Fltr.stylization, cleanliness: f.Fltr.cleanliness, brushScale: f.Fltr.brushScale, microBrush: f.Fltr.microBrush, lightDirection: f.Fltr.LghD, specularity: f.Fltr.specularity, }, }; case 'LqFy': { return { ...base, type: 'liquify', filter: { liquifyMesh: f.Fltr.LqMe, }, }; }; case 'perspectiveWarpTransform': { return { ...base, type: 'perspective warp', filter: { vertices: f.Fltr.vertices.map(hrznVrtcToPoint), warpedVertices: f.Fltr.warpedVertices.map(hrznVrtcToPoint), quads: f.Fltr.quads.map(q => q.indices), }, }; }; case 'Crvs': { return { ...base, type: 'curves', filter: { presetKind: presetKindType.decode(f.Fltr.presetKind), ...(f.Fltr.Adjs ? { adjustments: f.Fltr.Adjs.map(a => { const channels = a.Chnl.map(Chnl.decode); if (a['Crv ']) { return { channels, curve: a['Crv '].map(c => { const point: { x: number; y: number; curved?: boolean; } = { x: c.Hrzn, y: c.Vrtc }; if (c.Cnty) point.curved = true; return point; }), }; } else if (a.Mpng) { return { channels, values: a.Mpng }; } else { throw new Error(`Unknown curve adjustment`); } }) } : {}), }, }; }; case 'BrgC': { return { ...base, type: 'brightness/contrast', filter: { brightness: f.Fltr.Brgh, contrast: f.Fltr.Cntr, useLegacy: !!f.Fltr.useLegacy, }, }; }; default: if (options.throwForMissingFeatures) { // console.log('FILTER', require('util').inspect(f, false, 99, true)); throw new Error(`Unknown filter classId: ${(f as any).Fltr._classID}`); } return undefined; } } else { switch (f.filterID) { case 1098281575: return { ...base, type: 'average' }; case 1114403360: return { ...base, type: 'blur' }; case 1114403405: return { ...base, type: 'blur more' }; case 1148416099: return { ...base, type: 'despeckle' }; case 1180922912: return { ...base, type: 'facet' }; case 1181902701: return { ...base, type: 'fragment' }; case 1399353968: return { ...base, type: 'sharpen' }; case 1399353925: return { ...base, type: 'sharpen edges' }; case 1399353933: return { ...base, type: 'sharpen more' }; case 1181639749: return { ...base, type: 'find edges' }; case 1399616122: return { ...base, type: 'solarize' }; case 1314149187: return { ...base, type: 'ntsc colors' }; case 1231976050: return { ...base, type: 'invert' } default: if (options.throwForMissingFeatures) { // console.log('FILTER', require('util').inspect(f, false, 99, true)); throw new Error(`Unknown filterID: ${(f as any).filterID}`); } } } } function parseFilterFX(desc: SoLdDescriptorFilter, options: ReadOptions): PlacedLayerFilter { return { enabled: desc.enab, validAtPosition: desc.validAtPosition, maskEnabled: desc.filterMaskEnable, maskLinked: desc.filterMaskLinked, maskExtendWithWhite: desc.filterMaskExtendWithWhite, list: desc.filterFXList.map(x => parseFilterFXItem(x, options)).filter((x): x is Filter => !!x), }; } function uvRadius(t: { radius: UnitsValue; }) { return unitsValue(t.radius, 'radius'); } function serializeFilterFXItem(f: Filter): SoLdDescriptorFilterItem { const base: Omit = { _name: '', _classID: 'filterFX', 'Nm ': f.name, blendOptions: { _name: '', _classID: 'blendOptions', Opct: unitsPercentF(f.opacity), 'Md ': BlnM.encode(f.blendMode), }, enab: f.enabled, hasoptions: f.hasOptions, FrgC: serializeColor(f.foregroundColor), BckC: serializeColor(f.backgroundColor), }; switch (f.type) { case 'average': return { ...base, filterID: 1098281575 }; case 'blur': return { ...base, filterID: 1114403360 }; case 'blur more': return { ...base, filterID: 1114403405 }; case 'box blur': return { ...base, Fltr: { _name: 'Box Blur', _classID: 'boxblur', 'Rds ': uvRadius(f.filter), }, filterID: 697, }; case 'gaussian blur': return { ...base, Fltr: { // _name: '高斯模糊', // Testing _name: 'Gaussian Blur', _classID: 'GsnB', 'Rds ': uvRadius(f.filter), }, filterID: 1198747202, }; case 'motion blur': return { ...base, Fltr: { _name: 'Motion Blur', _classID: 'MtnB', Angl: f.filter.angle, Dstn: unitsValue(f.filter.distance, 'distance'), }, filterID: 1299476034, }; case 'radial blur': return { ...base, Fltr: { _name: 'Radial Blur', _classID: 'RdlB', Amnt: f.filter.amount, BlrM: BlrM.encode(f.filter.method), BlrQ: BlrQ.encode(f.filter.quality), }, filterID: 1382313026, }; case 'shape blur': return { ...base, Fltr: { _name: 'Shape Blur', _classID: 'shapeBlur', 'Rds ': uvRadius(f.filter), customShape: { _name: '', _classID: 'customShape', 'Nm ': f.filter.customShape.name, Idnt: f.filter.customShape.id, } }, filterID: 702, }; case 'smart blur': return { ...base, Fltr: { _name: 'Smart Blur', _classID: 'SmrB', 'Rds ': f.filter.radius, Thsh: f.filter.threshold, SmBQ: SmBQ.encode(f.filter.quality), SmBM: SmBM.encode(f.filter.mode), }, filterID: 1399681602, }; case 'surface blur': return { ...base, Fltr: { _name: 'Surface Blur', _classID: 'surfaceBlur', 'Rds ': uvRadius(f.filter), Thsh: f.filter.threshold, }, filterID: 701, }; case 'displace': return { ...base, Fltr: { _name: 'Displace', _classID: 'Dspl', HrzS: f.filter.horizontalScale, VrtS: f.filter.verticalScale, DspM: DspM.encode(f.filter.displacementMap), UndA: UndA.encode(f.filter.undefinedAreas), DspF: { sig: f.filter.displacementFile.signature, path: f.filter.displacementFile.path, }, }, filterID: 1148416108, }; case 'pinch': return { ...base, Fltr: { _name: 'Pinch', _classID: 'Pnch', Amnt: f.filter.amount, }, filterID: 1349411688, }; case 'polar coordinates': return { ...base, Fltr: { _name: 'Polar Coordinates', _classID: 'Plr ', Cnvr: Cnvr.encode(f.filter.conversion), }, filterID: 1349284384, }; case 'ripple': return { ...base, Fltr: { _name: 'Ripple', _classID: 'Rple', Amnt: f.filter.amount, RplS: RplS.encode(f.filter.size), }, filterID: 1383099493, }; case 'shear': return { ...base, Fltr: { _name: 'Shear', _classID: 'Shr ', ShrP: f.filter.shearPoints.map(p => ({ _name: '', _classID: 'Pnt ', Hrzn: p.x, Vrtc: p.y })), UndA: UndA.encode(f.filter.undefinedAreas), ShrS: f.filter.shearStart, ShrE: f.filter.shearEnd, }, filterID: 1399353888, }; case 'spherize': return { ...base, Fltr: { _name: 'Spherize', _classID: 'Sphr', Amnt: f.filter.amount, SphM: SphM.encode(f.filter.mode), }, filterID: 1399875698, }; case 'twirl': return { ...base, Fltr: { _name: 'Twirl', _classID: 'Twrl', Angl: f.filter.angle, }, filterID: 1417114220, }; case 'wave': return { ...base, Fltr: { _name: 'Wave', _classID: 'Wave', Wvtp: Wvtp.encode(f.filter.type), NmbG: f.filter.numberOfGenerators, WLMn: f.filter.wavelength.min, WLMx: f.filter.wavelength.max, AmMn: f.filter.amplitude.min, AmMx: f.filter.amplitude.max, SclH: f.filter.scale.x, SclV: f.filter.scale.y, UndA: UndA.encode(f.filter.undefinedAreas), RndS: f.filter.randomSeed, }, filterID: 1466005093, }; case 'zigzag': return { ...base, Fltr: { _name: 'ZigZag', _classID: 'ZgZg', Amnt: f.filter.amount, NmbR: f.filter.ridges, ZZTy: ZZTy.encode(f.filter.style), }, filterID: 1516722791, }; case 'add noise': return { ...base, Fltr: { _name: 'Add Noise', _classID: 'AdNs', Dstr: Dstr.encode(f.filter.distribution), Nose: unitsPercentF(f.filter.amount), Mnch: f.filter.monochromatic, FlRs: f.filter.randomSeed, }, filterID: 1097092723, }; case 'despeckle': return { ...base, filterID: 1148416099 }; case 'dust and scratches': return { ...base, Fltr: { _name: 'Dust & Scratches', _classID: 'DstS', 'Rds ': f.filter.radius, Thsh: f.filter.threshold, }, filterID: 1148417107, }; case 'median': return { ...base, Fltr: { _name: 'Median', _classID: 'Mdn ', 'Rds ': uvRadius(f.filter), }, filterID: 1298427424, }; case 'reduce noise': return { ...base, Fltr: { _name: 'Reduce Noise', _classID: 'denoise', ClNs: unitsPercentF(f.filter.reduceColorNoise), Shrp: unitsPercentF(f.filter.sharpenDetails), removeJPEGArtifact: f.filter.removeJpegArtifact, channelDenoise: f.filter.channelDenoise.map(c => ({ _name: '', _classID: 'channelDenoiseParams', Chnl: c.channels.map(i => Chnl.encode(i)), Amnt: c.amount, ...(c.preserveDetails ? { EdgF: c.preserveDetails } : {}), })), preset: f.filter.preset, }, filterID: 633, }; case 'color halftone': return { ...base, Fltr: { _name: 'Color Halftone', _classID: 'ClrH', 'Rds ': f.filter.radius, Ang1: f.filter.angle1, Ang2: f.filter.angle2, Ang3: f.filter.angle3, Ang4: f.filter.angle4, }, filterID: 1131180616, }; case 'crystallize': return { ...base, Fltr: { _name: 'Crystallize', _classID: 'Crst', ClSz: f.filter.cellSize, FlRs: f.filter.randomSeed, }, filterID: 1131574132, }; case 'facet': return { ...base, filterID: 1180922912 }; case 'fragment': return { ...base, filterID: 1181902701 }; case 'mezzotint': return { ...base, Fltr: { _name: 'Mezzotint', _classID: 'Mztn', MztT: MztT.encode(f.filter.type), FlRs: f.filter.randomSeed, }, filterID: 1299870830, }; case 'mosaic': return { ...base, Fltr: { _name: 'Mosaic', _classID: 'Msc ', ClSz: unitsValue(f.filter.cellSize, 'cellSize'), }, filterID: 1299407648, }; case 'pointillize': return { ...base, Fltr: { _name: 'Pointillize', _classID: 'Pntl', ClSz: f.filter.cellSize, FlRs: f.filter.randomSeed, }, filterID: 1349416044, }; case 'clouds': return { ...base, Fltr: { _name: 'Clouds', _classID: 'Clds', FlRs: f.filter.randomSeed, }, filterID: 1131177075, }; case 'difference clouds': return { ...base, Fltr: { _name: 'Difference Clouds', _classID: 'DfrC', FlRs: f.filter.randomSeed, }, filterID: 1147564611, }; case 'fibers': return { ...base, Fltr: { _name: 'Fibers', _classID: 'Fbrs', Vrnc: f.filter.variance, Strg: f.filter.strength, RndS: f.filter.randomSeed, }, filterID: 1180856947, }; case 'lens flare': return { ...base, Fltr: { _name: 'Lens Flare', _classID: 'LnsF', Brgh: f.filter.brightness, FlrC: { _name: '', _classID: 'Pnt ', Hrzn: f.filter.position.x, Vrtc: f.filter.position.y, }, 'Lns ': Lns.encode(f.filter.lensType), }, filterID: 1282306886, }; case 'sharpen': return { ...base, filterID: 1399353968 }; case 'sharpen edges': return { ...base, filterID: 1399353925 }; case 'sharpen more': return { ...base, filterID: 1399353933 }; case 'smart sharpen': return { ...base, Fltr: { _name: 'Smart Sharpen', _classID: 'smartSharpen', Amnt: unitsPercentF(f.filter.amount), 'Rds ': uvRadius(f.filter), Thsh: f.filter.threshold, Angl: f.filter.angle, moreAccurate: f.filter.moreAccurate, blur: blurType.encode(f.filter.blur), preset: f.filter.preset, sdwM: { _name: 'Parameters', _classID: 'adaptCorrectTones', Amnt: unitsPercentF(f.filter.shadow.fadeAmount), Wdth: unitsPercentF(f.filter.shadow.tonalWidth), 'Rds ': f.filter.shadow.radius, }, hglM: { _name: 'Parameters', _classID: 'adaptCorrectTones', Amnt: unitsPercentF(f.filter.highlight.fadeAmount), Wdth: unitsPercentF(f.filter.highlight.tonalWidth), 'Rds ': f.filter.highlight.radius, }, }, filterID: 698, }; case 'unsharp mask': return { ...base, Fltr: { _name: 'Unsharp Mask', _classID: 'UnsM', Amnt: unitsPercentF(f.filter.amount), 'Rds ': uvRadius(f.filter), Thsh: f.filter.threshold, }, filterID: 1433301837, }; case 'diffuse': return { ...base, Fltr: { _name: 'Diffuse', _classID: 'Dfs ', 'Md ': DfsM.encode(f.filter.mode), FlRs: f.filter.randomSeed, }, filterID: 1147564832, }; case 'emboss': return { ...base, Fltr: { _name: 'Emboss', _classID: 'Embs', Angl: f.filter.angle, Hght: f.filter.height, Amnt: f.filter.amount, }, filterID: 1164796531, }; case 'extrude': return { ...base, Fltr: { _name: 'Extrude', _classID: 'Extr', ExtS: f.filter.size, ExtD: f.filter.depth, ExtF: f.filter.solidFrontFaces, ExtM: f.filter.maskIncompleteBlocks, ExtT: ExtT.encode(f.filter.type), ExtR: ExtR.encode(f.filter.depthMode), FlRs: f.filter.randomSeed, }, filterID: 1165522034, }; case 'find edges': return { ...base, filterID: 1181639749 }; case 'solarize': return { ...base, filterID: 1399616122 }; case 'tiles': return { ...base, Fltr: { _name: 'Tiles', _classID: 'Tls ', TlNm: f.filter.numberOfTiles, TlOf: f.filter.maximumOffset, FlCl: FlCl.encode(f.filter.fillEmptyAreaWith), FlRs: f.filter.randomSeed, }, filterID: 1416393504, }; case 'trace contour': return { ...base, Fltr: { _name: 'Trace Contour', _classID: 'TrcC', 'Lvl ': f.filter.level, 'Edg ': CntE.encode(f.filter.edge), }, filterID: 1416782659, }; case 'wind': return { ...base, Fltr: { _name: 'Wind', _classID: 'Wnd ', WndM: WndM.encode(f.filter.method), Drct: Drct.encode(f.filter.direction), }, filterID: 1466852384, }; case 'de-interlace': return { ...base, Fltr: { _name: 'De-Interlace', _classID: 'Dntr', IntE: IntE.encode(f.filter.eliminate), IntC: IntC.encode(f.filter.newFieldsBy), }, filterID: 1148089458, }; case 'ntsc colors': return { ...base, filterID: 1314149187 }; case 'invert': return { ...base, filterID: 1231976050 }; case 'custom': return { ...base, Fltr: { _name: 'Custom', _classID: 'Cstm', 'Scl ': f.filter.scale, Ofst: f.filter.offset, Mtrx: f.filter.matrix, }, filterID: 1131639917, }; case 'high pass': return { ...base, Fltr: { _name: 'High Pass', _classID: 'HghP', 'Rds ': uvRadius(f.filter), }, filterID: 1214736464, }; case 'maximum': return { ...base, Fltr: { _name: 'Maximum', _classID: 'Mxm ', 'Rds ': uvRadius(f.filter), }, filterID: 1299737888, }; case 'minimum': return { ...base, Fltr: { _name: 'Minimum', _classID: 'Mnm ', 'Rds ': uvRadius(f.filter), }, filterID: 1299082528, }; case 'offset': return { ...base, Fltr: { _name: 'Offset', _classID: 'Ofst', Hrzn: f.filter.horizontal, Vrtc: f.filter.vertical, 'Fl ': FlMd.encode(f.filter.undefinedAreas), }, filterID: 1332114292, }; case 'puppet': return { ...base, Fltr: { _name: 'Rigid Transform', _classID: 'rigidTransform', 'null': ['Ordn.Trgt'], // TODO: ??? rigidType: f.filter.rigidType, puppetShapeList: f.filter.puppetShapeList.map(p => ({ _name: '', _classID: 'puppetShape', rigidType: p.rigidType, VrsM: 1, // TODO: ??? VrsN: 0, // TODO: ??? originalVertexArray: toUint8(new Float32Array(pointsToArray(p.originalVertexArray))), deformedVertexArray: toUint8(new Float32Array(pointsToArray(p.deformedVertexArray))), indexArray: toUint8(new Uint32Array(p.indexArray)), pinOffsets: pointsToArray(p.pinOffsets), posFinalPins: pointsToArray(p.posFinalPins), pinVertexIndices: p.pinVertexIndices, PinP: pointsToArray(p.pinPosition), PnRt: p.pinRotation, PnOv: p.pinOverlay, PnDp: p.pinDepth, meshQuality: p.meshQuality, meshExpansion: p.meshExpansion, meshRigidity: p.meshRigidity, imageResolution: p.imageResolution, meshBoundaryPath: { _name: '', _classID: 'pathClass', pathComponents: p.meshBoundaryPath.pathComponents.map(c => ({ _name: '', _classID: 'PaCm', shapeOperation: `shapeOperation.${c.shapeOperation}`, SbpL: c.paths.map(path => ({ _name: '', _classID: 'Sbpl', Clsp: path.closed, 'Pts ': path.points.map(pt => ({ _name: '', _classID: 'Pthp', Anch: pointToHrznVrtc(pt.anchor), 'Fwd ': pointToHrznVrtc(pt.forward), 'Bwd ': pointToHrznVrtc(pt.backward), Smoo: pt.smooth, })), })), })), }, selectedPin: p.selectedPin, })), PuX0: f.filter.bounds[0].x, PuX1: f.filter.bounds[1].x, PuX2: f.filter.bounds[2].x, PuX3: f.filter.bounds[3].x, PuY0: f.filter.bounds[0].y, PuY1: f.filter.bounds[1].y, PuY2: f.filter.bounds[2].y, PuY3: f.filter.bounds[3].y, }, filterID: 991, }; case 'oil paint plugin': { const params: any = {}; for (let i = 0; i < f.filter.parameters.length; i++) { const { name, value } = f.filter.parameters[i]; const suffix = `${fromAtoZ[Math.floor(i / fromAtoZ.length)]}${fromAtoZ[i % fromAtoZ.length]}`; params[`PN${suffix}`] = name; params[`PT${suffix}`] = 0; params[`PF${suffix}`] = value; } return { ...base, Fltr: { _name: 'Oil Paint Plugin', _classID: 'PbPl', KnNm: f.filter.name, GpuY: f.filter.gpu, LIWy: f.filter.lighting, FPth: '1', ...params, }, filterID: 1348620396, }; } case 'oil paint': return { ...base, Fltr: { _name: 'Oil Paint', _classID: 'oilPaint', lightingOn: f.filter.lightingOn, stylization: f.filter.stylization, cleanliness: f.filter.cleanliness, brushScale: f.filter.brushScale, microBrush: f.filter.microBrush, LghD: f.filter.lightDirection, specularity: f.filter.specularity, }, filterID: 1122, }; case 'liquify': return { ...base, Fltr: { _name: 'Liquify', _classID: 'LqFy', LqMe: f.filter.liquifyMesh, }, filterID: 1282492025, }; case 'perspective warp': return { ...base, Fltr: { _name: 'Perspective Warp', _classID: 'perspectiveWarpTransform', vertices: f.filter.vertices.map(pointToHrznVrtc), warpedVertices: f.filter.warpedVertices.map(pointToHrznVrtc), quads: f.filter.quads.map(indices => ({ indices })), }, filterID: 442, }; case 'curves': return { ...base, Fltr: { _name: 'Curves', _classID: 'Crvs', presetKind: presetKindType.encode(f.filter.presetKind), ...(f.filter.adjustments ? { Adjs: f.filter.adjustments.map(a => 'curve' in a ? { _name: '', _classID: 'CrvA', Chnl: a.channels.map(Chnl.encode), 'Crv ': a.curve.map(c => ({ _name: '', _classID: 'Pnt ', Hrzn: c.x, Vrtc: c.y, ...(c.curved ? { Cnty: true } : {}), }) as FilterCurvesCurvePoint), } : { _name: '', _classID: 'CrvA', Chnl: a.channels.map(Chnl.encode), Mpng: a.values, }) } : {}), }, filterID: 1131574899, }; case 'brightness/contrast': return { ...base, Fltr: { _name: 'Brightness/Contrast', _classID: 'BrgC', Brgh: f.filter.brightness, Cntr: f.filter.contrast, useLegacy: !!f.filter.useLegacy, }, filterID: 1114793795, }; // case 'hsb/hsl': return { // TODO: ... // }; default: throw new Error(`Unknow filter type: ${(f as any).type}`); } } interface SoLdDescriptor { Idnt: string; placed: string; PgNm: number; totalPages: number; Crop?: number; frameStep: FractionDescriptor; duration: FractionDescriptor; frameCount: number; Annt: number; Type: number; Trnf: number[]; nonAffineTransform: number[]; quiltWarp?: QuiltWarpDescriptor; warp: WarpDescriptor; 'Sz ': { _name: '', _classID: 'Pnt ', Wdth: number; Hght: number; }; Rslt: DescriptorUnitsValue; filterFX?: SoLdDescriptorFilter; comp?: number; // TODO: support this ? compInfo?: { compID: number; originalCompID: number; }; // TODO: support this ? Impr?: {}; // ??? ClMg?: { // TODO: support this ? _name: ''; _classID: 'ClMg'; placedLayerOCIOConversion: string; // 'placedLayerOCIOConversion.placedLayerOCIOConvertEmbedded' }; } // let t: any; function getWarpFromPlacedLayer(placed: PlacedLayer): Warp { if (placed.warp) return placed.warp; if (!placed.width || !placed.height) throw new Error('You must provide width and height of the linked image in placedLayer'); const w = placed.width; const h = placed.height; const x0 = 0, x1 = w / 3, x2 = w * 2 / 3, x3 = w; const y0 = 0, y1 = h / 3, y2 = h * 2 / 3, y3 = h; return { style: 'custom', value: 0, perspective: 0, perspectiveOther: 0, rotate: 'horizontal', bounds: { top: { value: 0, units: 'Pixels' }, left: { value: 0, units: 'Pixels' }, bottom: { value: h, units: 'Pixels' }, right: { value: w, units: 'Pixels' }, }, uOrder: 4, vOrder: 4, customEnvelopeWarp: { meshPoints: [ { x: x0, y: y0 }, { x: x1, y: y0 }, { x: x2, y: y0 }, { x: x3, y: y0 }, { x: x0, y: y1 }, { x: x1, y: y1 }, { x: x2, y: y1 }, { x: x3, y: y1 }, { x: x0, y: y2 }, { x: x1, y: y2 }, { x: x2, y: y2 }, { x: x3, y: y2 }, { x: x0, y: y3 }, { x: x1, y: y3 }, { x: x2, y: y3 }, { x: x3, y: y3 }, ], }, }; } addHandler( 'SoLd', hasKey('placedLayer'), (reader, target, left) => { if (readSignature(reader) !== 'soLD') throw new Error(`Invalid SoLd type`); const version = readInt32(reader); if (version !== 4 && version !== 5) throw new Error(`Invalid SoLd version`); const desc: SoLdDescriptor = readVersionAndDescriptor(reader, true); // console.log('SoLd', require('util').inspect(desc, false, 99, true)); // console.log('SoLd.warp', require('util').inspect(desc.warp, false, 99, true)); // console.log('SoLd.quiltWarp', require('util').inspect(desc.quiltWarp, false, 99, true)); // desc.filterFX!.filterFXList[0].Fltr.puppetShapeList[0].meshBoundaryPath.pathComponents[0].SbpL[0]['Pts '] = []; // console.log('read', require('util').inspect(desc.filterFX, false, 99, true)); // console.log('filterFXList[0]', require('util').inspect((desc as any).filterFX.filterFXList[0], false, 99, true)); // t = desc; target.placedLayer = { id: desc.Idnt, placed: desc.placed, type: placedLayerTypes[desc.Type], pageNumber: desc.PgNm, totalPages: desc.totalPages, frameStep: frac(desc.frameStep), duration: frac(desc.duration), frameCount: desc.frameCount, transform: desc.Trnf, width: desc['Sz '].Wdth, height: desc['Sz '].Hght, resolution: parseUnits(desc.Rslt), warp: parseWarp((desc.quiltWarp || desc.warp) as any), }; if (desc.nonAffineTransform && desc.nonAffineTransform.some((x, i) => x !== desc.Trnf[i])) { target.placedLayer.nonAffineTransform = desc.nonAffineTransform; } if (desc.Crop) target.placedLayer.crop = desc.Crop; if (desc.comp) target.placedLayer.comp = desc.comp; if (desc.compInfo) { target.placedLayer.compInfo = { compID: desc.compInfo.compID, originalCompID: desc.compInfo.originalCompID, }; } if (desc.filterFX) target.placedLayer.filter = parseFilterFX(desc.filterFX, reader); // console.log('filter', require('util').inspect(target.placedLayer.filter, false, 99, true)); skipBytes(reader, left()); // HACK }, (writer, target) => { writeSignature(writer, 'soLD'); writeInt32(writer, 4); // version const placed = target.placedLayer!; if (!placed.id || typeof placed.id !== 'string' || !/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(placed.id)) { throw new Error('Placed layer ID must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4)'); } const desc: SoLdDescriptor = { Idnt: placed.id, placed: placed.placed ?? placed.id, PgNm: placed.pageNumber || 1, totalPages: placed.totalPages || 1, ...(placed.crop ? { Crop: placed.crop } : {}), frameStep: placed.frameStep || { numerator: 0, denominator: 600 }, duration: placed.duration || { numerator: 0, denominator: 600 }, frameCount: placed.frameCount || 0, Annt: 16, Type: placedLayerTypes.indexOf(placed.type), Trnf: placed.transform, nonAffineTransform: placed.nonAffineTransform ?? placed.transform, // quiltWarp: {} as any, warp: encodeWarp(getWarpFromPlacedLayer(placed)), 'Sz ': { _name: '', _classID: 'Pnt ', Wdth: placed.width || 0, // TODO: find size ? Hght: placed.height || 0, // TODO: find size ? }, Rslt: placed.resolution ? unitsValue(placed.resolution, 'resolution') : { units: 'Density', value: 72 }, }; if (placed.filter) { desc.filterFX = { _name: '', _classID: 'filterFXStyle', enab: placed.filter.enabled, validAtPosition: placed.filter.validAtPosition, filterMaskEnable: placed.filter.maskEnabled, filterMaskLinked: placed.filter.maskLinked, filterMaskExtendWithWhite: placed.filter.maskExtendWithWhite, filterFXList: placed.filter.list.map(f => serializeFilterFXItem(f)), }; } // TODO: // desc.comp = -1; // desc.compInfo = { _name: '', _classID: 'null', compID: -1, originalCompID: -1 } as any; // desc.ClMg = { // _name: '', // _classID: 'ClMg', // placedLayerOCIOConversion: 'placedLayerOCIOConversion.placedLayerOCIOConvertEmbedded' // } as any; // if (JSON.stringify(t) !== JSON.stringify(desc)) { // console.log('read', require('util').inspect(t, false, 99, true)); // console.log('write', require('util').inspect(desc, false, 99, true)); // console.error('DIFFERENT'); // // throw new Error('DIFFERENT'); // } if (placed.warp && isQuiltWarp(placed.warp)) { const quiltWarp = encodeWarp(placed.warp) as QuiltWarpDescriptor; desc.quiltWarp = quiltWarp; desc.warp = { warpStyle: 'warpStyle.warpNone', warpValue: quiltWarp.warpValue, warpPerspective: quiltWarp.warpPerspective, warpPerspectiveOther: quiltWarp.warpPerspectiveOther, warpRotate: quiltWarp.warpRotate, bounds: quiltWarp.bounds, uOrder: quiltWarp.uOrder, vOrder: quiltWarp.vOrder, }; } else { delete desc.quiltWarp; } if (placed.comp) desc.comp = placed.comp; if (placed.compInfo) desc.compInfo = placed.compInfo; writeVersionAndDescriptor(writer, '', 'null', desc, desc.quiltWarp ? 'quiltWarp' : 'warp'); }, ); addHandlerAlias('SoLE', 'SoLd'); addHandler( 'fxrp', hasKey('referencePoint'), (reader, target) => { target.referencePoint = { x: readFloat64(reader), y: readFloat64(reader), }; }, (writer, target) => { writeFloat64(writer, target.referencePoint!.x); writeFloat64(writer, target.referencePoint!.y); }, ); addHandler( 'Lr16', () => false, (reader, _target, _left, psd, imageResources) => { readLayerInfo(reader, psd, imageResources); }, (_writer, _target) => { }, ); addHandler( 'Lr32', () => false, (reader, _target, _left, psd, imageResources) => { readLayerInfo(reader, psd, imageResources); }, (_writer, _target) => { }, ); addHandler( 'LMsk', hasKey('userMask'), (reader, target) => { target.userMask = { colorSpace: readColor(reader), opacity: readUint16(reader) / 0xff, }; const flag = readUint8(reader); if (flag !== 128) throw new Error('Invalid flag value'); skipBytes(reader, 1); }, (writer, target) => { const userMask = target.userMask!; writeColor(writer, userMask.colorSpace); writeUint16(writer, clamp(userMask.opacity, 0, 1) * 0xff); writeUint8(writer, 128); writeZeros(writer, 1); }, ); if (MOCK_HANDLERS) { addHandler( 'Patt', target => (target as any)._Patt !== undefined, (reader, target, left) => { // console.log('additional info: Patt'); (target as any)._Patt = readBytes(reader, left()); }, (writer, target) => false && writeBytes(writer, (target as any)._Patt), ); } else { addHandler( 'Patt', // TODO: handle also Pat2 & Pat3 target => !target, (reader, target, left) => { if (!left()) return; skipBytes(reader, left()); return; // not supported yet target; readPattern; // if (!target.patterns) target.patterns = []; // target.patterns.push(readPattern(reader)); // skipBytes(reader, left()); }, (_writer, _target) => { }, ); } /* interface CAIDesc { _name: ''; _classID: 'null'; enab: boolean; generationalGuid: string; } addHandler( 'CAI ', // content credentials ? something to do with generative tech () => false, (reader, _target, left) => { const version = readUint32(reader); // 3 const desc = readVersionAndDescriptor(reader, true) as CAIDesc; console.log('CAI version', version); console.log('CAI', require('util').inspect(desc, false, 99, true)); console.log('CAI left', readBytes(reader, left())); // 8 bytes left, all zeroes }, (_writer, _target) => { }, ); // */ if (MOCK_HANDLERS) { addHandler( 'CAI ', target => (target as any)._CAI_ !== undefined, (reader, target, left) => { (target as any)._CAI_ = readBytes(reader, left()); }, (writer, target) => { writeBytes(writer, (target as any)._CAI_); }, ); } // interface OCIODescriptor { // _name: ''; // _classID: 'documentColorManagementInfo'; // 'Knd ': 'icc'; // ocio_display_view: { // _name: ''; // _classID: 'viewColorManagementInfo'; // display: string; // view: string; // }; // } if (MOCK_HANDLERS) { addHandler( 'OCIO', // document color management info target => (target as any)._OCIO !== undefined, (reader, target, left) => { // const desc = readVersionAndDescriptor(reader, true) as OCIODescriptor; // console.log('OCIO', require('util').inspect(desc, false, 99, true)); (target as any)._OCIO = readBytes(reader, left()); }, (writer, target) => { writeBytes(writer, (target as any)._OCIO); }, ); } // interface GenIDescriptor { // _name: ''; // _classID: 'genTechInfo'; // isUsingGenTech: number; // externalModelList?: []; // } if (MOCK_HANDLERS) { addHandler( 'GenI', // generative tech target => (target as any)._GenI !== undefined, (reader, target, left) => { const desc = readVersionAndDescriptor(reader, true); // as GenIDescriptor; console.log('GenI', require('util').inspect(desc, false, 99, true)); (target as any)._GenI = readBytes(reader, left()); }, (writer, target) => { writeBytes(writer, (target as any)._GenI); }, ); } function readRect(reader: PsdReader) { const top = readInt32(reader); const left = readInt32(reader); const bottom = readInt32(reader); const right = readInt32(reader); return { top, left, bottom, right }; } function writeRect(writer: PsdWriter, rect: { left: number; top: number; right: number; bottom: number }) { writeInt32(writer, rect.top); writeInt32(writer, rect.left); writeInt32(writer, rect.bottom); writeInt32(writer, rect.right); } addHandler( 'Anno', target => (target as Psd).annotations !== undefined, (reader, target, left) => { const major = readUint16(reader); const minor = readUint16(reader); if (major !== 2 || minor !== 1) throw new Error('Invalid Anno version'); const count = readUint32(reader); const annotations: Annotation[] = []; for (let i = 0; i < count; i++) { /*const length =*/ readUint32(reader); const type = readSignature(reader); const open = !!readUint8(reader); /*const flags =*/ readUint8(reader); // always 28 /*const optionalBlocks =*/ readUint16(reader); const iconLocation = readRect(reader); const popupLocation = readRect(reader); const color = readColor(reader); const author = readPascalString(reader, 2); const name = readPascalString(reader, 2); const date = readPascalString(reader, 2); /*const contentLength =*/ readUint32(reader); /*const dataType =*/ readSignature(reader); const dataLength = readUint32(reader); let data: string | Uint8Array; if (type === 'txtA') { if (dataLength >= 2 && readUint16(reader) === 0xfeff) { data = readUnicodeStringWithLength(reader, (dataLength - 2) / 2); } else { reader.offset -= 2; data = readAsciiString(reader, dataLength); } data = data.replace(/\r/g, '\n'); } else if (type === 'sndA') { data = readBytes(reader, dataLength); } else { throw new Error('Unknown annotation type'); } annotations.push({ type: type === 'txtA' ? 'text' : 'sound', open, iconLocation, popupLocation, color, author, name, date, data, }); } (target as Psd).annotations = annotations; skipBytes(reader, left()); }, (writer, target) => { const annotations = (target as Psd).annotations!; writeUint16(writer, 2); writeUint16(writer, 1); writeUint32(writer, annotations.length); for (const annotation of annotations) { const sound = annotation.type === 'sound'; if (sound && !(annotation.data instanceof Uint8Array)) throw new Error('Sound annotation data should be Uint8Array'); if (!sound && typeof annotation.data !== 'string') throw new Error('Text annotation data should be string'); const lengthOffset = writer.offset; writeUint32(writer, 0); // length writeSignature(writer, sound ? 'sndA' : 'txtA'); writeUint8(writer, annotation.open ? 1 : 0); writeUint8(writer, 28); writeUint16(writer, 1); writeRect(writer, annotation.iconLocation); writeRect(writer, annotation.popupLocation); writeColor(writer, annotation.color); writePascalString(writer, annotation.author || '', 2); writePascalString(writer, annotation.name || '', 2); writePascalString(writer, annotation.date || '', 2); const contentOffset = writer.offset; writeUint32(writer, 0); // content length writeSignature(writer, sound ? 'sndM' : 'txtC'); writeUint32(writer, 0); // data length const dataOffset = writer.offset; if (sound) { writeBytes(writer, annotation.data as Uint8Array); } else { writeUint16(writer, 0xfeff); // unicode string indicator const text = (annotation.data as string).replace(/\n/g, '\r'); for (let i = 0; i < text.length; i++) writeUint16(writer, text.charCodeAt(i)); } writer.view.setUint32(lengthOffset, writer.offset - lengthOffset, false); writer.view.setUint32(contentOffset, writer.offset - contentOffset, false); writer.view.setUint32(dataOffset - 4, writer.offset - dataOffset, false); } } ); interface FileOpenDescriptor { compInfo: { compID: number; originalCompID: number; }; } interface LinkedFileDescriptor { descVersion: 2; 'Nm ': string; fullPath: string; originalPath: string; relPath: string; } function createLnkHandler(tag: string) { addHandler( tag, (target: any) => { const psd = target as Psd; if (!psd.linkedFiles || !psd.linkedFiles.length) return false; if (tag === 'lnkE' && !psd.linkedFiles.some(f => f.linkedFile)) return false; return true; }, (reader, target, left, _psd) => { const psd = target as Psd; psd.linkedFiles = psd.linkedFiles || []; while (left() > 8) { let size = readLength64(reader); const startOffset = reader.offset; const type = readSignature(reader) as 'liFD' | 'liFE' | 'liFA'; // liFD - linked file data // liFE - linked file external // liFA - linked file alias const version = readInt32(reader); const id = readPascalString(reader, 1); const name = readUnicodeString(reader); const fileType = readSignature(reader).trim(); // ' ' if empty const fileCreator = readSignature(reader).trim(); // ' ' or '\0\0\0\0' if empty const dataSize = readLength64(reader); const hasFileOpenDescriptor = readUint8(reader); const fileOpenDescriptor = hasFileOpenDescriptor ? readVersionAndDescriptor(reader) as FileOpenDescriptor : undefined; const linkedFileDescriptor = type === 'liFE' ? readVersionAndDescriptor(reader) as LinkedFileDescriptor : undefined; const file: LinkedFile = { id, name }; if (fileType) file.type = fileType; if (fileCreator) file.creator = fileCreator; if (fileOpenDescriptor) { file.descriptor = { compInfo: { compID: fileOpenDescriptor.compInfo.compID, originalCompID: fileOpenDescriptor.compInfo.originalCompID, } }; } if (type === 'liFE' && version > 3) { const year = readInt32(reader); const month = readUint8(reader); const day = readUint8(reader); const hour = readUint8(reader); const minute = readUint8(reader); const seconds = readFloat64(reader); const wholeSeconds = Math.floor(seconds); const ms = (seconds - wholeSeconds) * 1000; file.time = (new Date(Date.UTC(year, month, day, hour, minute, wholeSeconds, ms))).toISOString(); } const fileSize = type === 'liFE' ? readLength64(reader) : 0; if (type === 'liFA') skipBytes(reader, 8); if (type === 'liFD') file.data = readBytes(reader, dataSize); // seems to be a typo in docs if (version >= 5) file.childDocumentID = readUnicodeString(reader); if (version >= 6) file.assetModTime = readFloat64(reader); if (version >= 7) file.assetLockedState = readUint8(reader); if (type === 'liFE' && version === 2) file.data = readBytes(reader, fileSize); if (reader.skipLinkedFilesData) file.data = undefined; if (tag === 'lnkE') { file.linkedFile = { fileSize, name: linkedFileDescriptor?.['Nm '] || '', fullPath: linkedFileDescriptor?.fullPath || '', originalPath: linkedFileDescriptor?.originalPath || '', relativePath: linkedFileDescriptor?.relPath || '', }; } psd.linkedFiles.push(file); while (size % 4) size++; reader.offset = startOffset + size; } skipBytes(reader, left()); // ? }, (writer, target) => { const psd = target as Psd; for (const file of psd.linkedFiles!) { if ((tag === 'lnkE') !== !!file.linkedFile) continue; let version = 2; if (file.assetLockedState != null) version = 7; else if (file.assetModTime != null) version = 6; else if (file.childDocumentID != null) version = 5; else if (tag === 'lnkE') version = 3; writeLength64(writer, 0); const sizeOffset = writer.offset; writeSignature(writer, (tag === 'lnkE') ? 'liFE' : (file.data ? 'liFD' : 'liFA')); writeInt32(writer, version); if (!file.id || typeof file.id !== 'string' || !/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(file.id)) { throw new Error('Linked file ID must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4)'); } writePascalString(writer, file.id, 1); writeUnicodeStringWithPadding(writer, file.name || ''); writeSignature(writer, file.type ? `${file.type} `.substring(0, 4) : ' '); writeSignature(writer, file.creator ? `${file.creator} `.substring(0, 4) : '\0\0\0\0'); writeLength64(writer, file.data ? file.data.byteLength : 0); if (file.descriptor && file.descriptor.compInfo) { const desc: FileOpenDescriptor = { compInfo: { compID: file.descriptor.compInfo.compID, originalCompID: file.descriptor.compInfo.originalCompID, }, }; writeUint8(writer, 1); writeVersionAndDescriptor(writer, '', 'null', desc); } else { writeUint8(writer, 0); } if (tag === 'lnkE') { const desc: LinkedFileDescriptor = { descVersion: 2, 'Nm ': file.linkedFile?.name ?? '', fullPath: file.linkedFile?.fullPath ?? '', originalPath: file.linkedFile?.originalPath ?? '', relPath: file.linkedFile?.relativePath ?? '', }; writeVersionAndDescriptor(writer, '', 'ExternalFileLink', desc); const time = file.time ? new Date(file.time) : new Date(); writeInt32(writer, time.getUTCFullYear()); writeUint8(writer, time.getUTCMonth()); writeUint8(writer, time.getUTCDate()); writeUint8(writer, time.getUTCHours()); writeUint8(writer, time.getUTCMinutes()); writeFloat64(writer, time.getUTCSeconds() + time.getUTCMilliseconds() / 1000); } if (file.data) { writeBytes(writer, file.data); } else { writeLength64(writer, file.linkedFile?.fileSize || 0); } if (version >= 5) writeUnicodeStringWithPadding(writer, file.childDocumentID || ''); if (version >= 6) writeFloat64(writer, file.assetModTime || 0); if (version >= 7) writeUint8(writer, file.assetLockedState || 0); let size = writer.offset - sizeOffset; writer.view.setUint32(sizeOffset - 4, size, false); // write size while (size % 4) { size++; writeUint8(writer, 0); } } }, ); } createLnkHandler('lnk2'); createLnkHandler('lnkE'); addHandlerAlias('lnkD', 'lnk2'); addHandlerAlias('lnk3', 'lnk2'); interface PthsDescriptor { pathList: { _classID: 'pathInfoClass'; pathUnicodeName: string; pathSymmetryClass: { _classID: 'pathSymmetryClass'; pathSymmetryMode: string; // 'pathSymmetryModeEnum.pathSymmetryModeBasicPath' }; }[]; } addHandler( 'pths', hasKey('pathList'), (reader, target) => { const desc = readVersionAndDescriptor(reader, true) as PthsDescriptor; // console.log(require('util').inspect(desc, false, 99, true)); // if (options.throwForMissingFeatures && desc?.pathList?.length) throw new Error('non-empty pathList in `pths`'); desc; target.pathList = []; // TODO: read paths }, (writer, _target) => { const desc: PthsDescriptor = { pathList: [], // TODO: write paths }; writeVersionAndDescriptor(writer, '', 'pathsDataClass', desc); }, ); addHandler( 'lyvr', hasKey('version'), (reader, target) => target.version = readUint32(reader), (writer, target) => writeUint32(writer, target.version!), ); addHandler( 'lfxs', () => false, // TODO: not sure when we actually need to write this section // NOTE: this might be insufficient // target => target.effects !== undefined && ( // !!target.effects.dropShadow?.some(e => e.choke) || // !!target.effects.innerShadow?.some(e => e.choke) || // !!target.effects.outerGlow?.choke || // !!target.effects.innerGlow?.choke // ), (reader, target, left) => { const version = readUint32(reader); if (version !== 0) throw new Error(`Invalid lfxs version`); const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader); target.effects = parseEffects(desc, !!reader.logMissingFeatures); skipBytes(reader, left()); }, (writer, target, _, options) => { const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true); writeUint32(writer, 0); // version writeVersionAndDescriptor(writer, '', 'null', desc); }, ); function adjustmentType(type: string) { return (target: LayerAdditionalInfo) => !!target.adjustment && target.adjustment.type === type; } addHandler( 'brit', adjustmentType('brightness/contrast'), (reader, target, left) => { if (!target.adjustment) { // ignore if got one from CgEd block target.adjustment = { type: 'brightness/contrast', brightness: readInt16(reader), contrast: readInt16(reader), meanValue: readInt16(reader), labColorOnly: !!readUint8(reader), useLegacy: true, }; } skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as BrightnessAdjustment; writeInt16(writer, info.brightness || 0); writeInt16(writer, info.contrast || 0); writeInt16(writer, info.meanValue ?? 127); writeUint8(writer, info.labColorOnly ? 1 : 0); writeZeros(writer, 1); }, ); function readLevelsChannel(reader: PsdReader): LevelsAdjustmentChannel { const shadowInput = readInt16(reader); const highlightInput = readInt16(reader); const shadowOutput = readInt16(reader); const highlightOutput = readInt16(reader); const midtoneInput = readInt16(reader) / 100; return { shadowInput, highlightInput, shadowOutput, highlightOutput, midtoneInput }; } function writeLevelsChannel(writer: PsdWriter, channel: LevelsAdjustmentChannel) { writeInt16(writer, channel.shadowInput); writeInt16(writer, channel.highlightInput); writeInt16(writer, channel.shadowOutput); writeInt16(writer, channel.highlightOutput); writeInt16(writer, Math.round(channel.midtoneInput * 100)); } addHandler( 'levl', adjustmentType('levels'), (reader, target, left) => { if (readUint16(reader) !== 2) throw new Error('Invalid levl version'); target.adjustment = { ...target.adjustment as PresetInfo, type: 'levels', rgb: readLevelsChannel(reader), red: readLevelsChannel(reader), green: readLevelsChannel(reader), blue: readLevelsChannel(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as LevelsAdjustment; const defaultChannel = { shadowInput: 0, highlightInput: 255, shadowOutput: 0, highlightOutput: 255, midtoneInput: 1, }; writeUint16(writer, 2); // version writeLevelsChannel(writer, info.rgb || defaultChannel); writeLevelsChannel(writer, info.red || defaultChannel); writeLevelsChannel(writer, info.blue || defaultChannel); writeLevelsChannel(writer, info.green || defaultChannel); for (let i = 0; i < 59; i++) writeLevelsChannel(writer, defaultChannel); }, ); function readCurveChannel(reader: PsdReader) { const nodes = readUint16(reader); const channel: CurvesAdjustmentChannel = []; for (let j = 0; j < nodes; j++) { const output = readInt16(reader); const input = readInt16(reader); channel.push({ input, output }); } return channel; } function writeCurveChannel(writer: PsdWriter, channel: CurvesAdjustmentChannel) { writeUint16(writer, channel.length); for (const n of channel) { writeUint16(writer, n.output); writeUint16(writer, n.input); } } addHandler( 'curv', adjustmentType('curves'), (reader, target, left) => { readUint8(reader); if (readUint16(reader) !== 1) throw new Error('Invalid curv version'); readUint16(reader); const channels = readUint16(reader); const info: CurvesAdjustment = { type: 'curves' }; if (channels & 1) info.rgb = readCurveChannel(reader); if (channels & 2) info.red = readCurveChannel(reader); if (channels & 4) info.green = readCurveChannel(reader); if (channels & 8) info.blue = readCurveChannel(reader); target.adjustment = { ...target.adjustment as PresetInfo, ...info, }; // ignoring, duplicate information // checkSignature(reader, 'Crv '); // const cVersion = readUint16(reader); // readUint16(reader); // const channelCount = readUint16(reader); // for (let i = 0; i < channelCount; i++) { // const index = readUint16(reader); // const nodes = readUint16(reader); // for (let j = 0; j < nodes; j++) { // const output = readInt16(reader); // const input = readInt16(reader); // } // } skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as CurvesAdjustment; const { rgb, red, green, blue } = info; let channels = 0; let channelCount = 0; if (rgb && rgb.length) { channels |= 1; channelCount++; } if (red && red.length) { channels |= 2; channelCount++; } if (green && green.length) { channels |= 4; channelCount++; } if (blue && blue.length) { channels |= 8; channelCount++; } writeUint8(writer, 0); writeUint16(writer, 1); // version writeUint16(writer, 0); writeUint16(writer, channels); if (rgb && rgb.length) writeCurveChannel(writer, rgb); if (red && red.length) writeCurveChannel(writer, red); if (green && green.length) writeCurveChannel(writer, green); if (blue && blue.length) writeCurveChannel(writer, blue); writeSignature(writer, 'Crv '); writeUint16(writer, 4); // version writeUint16(writer, 0); writeUint16(writer, channelCount); if (rgb && rgb.length) { writeUint16(writer, 0); writeCurveChannel(writer, rgb); } if (red && red.length) { writeUint16(writer, 1); writeCurveChannel(writer, red); } if (green && green.length) { writeUint16(writer, 2); writeCurveChannel(writer, green); } if (blue && blue.length) { writeUint16(writer, 3); writeCurveChannel(writer, blue); } }, ); addHandler( 'expA', adjustmentType('exposure'), (reader, target, left) => { if (readUint16(reader) !== 1) throw new Error('Invalid expA version'); target.adjustment = { ...target.adjustment as PresetInfo, type: 'exposure', exposure: readFloat32(reader), offset: readFloat32(reader), gamma: readFloat32(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as ExposureAdjustment; writeUint16(writer, 1); // version writeFloat32(writer, info.exposure!); writeFloat32(writer, info.offset!); writeFloat32(writer, info.gamma!); writeZeros(writer, 2); }, ); interface VibranceDescriptor { vibrance?: number; Strt?: number; } addHandler( 'vibA', adjustmentType('vibrance'), (reader, target, left) => { const desc: VibranceDescriptor = readVersionAndDescriptor(reader); target.adjustment = { type: 'vibrance' }; if (desc.vibrance !== undefined) target.adjustment.vibrance = desc.vibrance; if (desc.Strt !== undefined) target.adjustment.saturation = desc.Strt; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as VibranceAdjustment; const desc: VibranceDescriptor = {}; if (info.vibrance !== undefined) desc.vibrance = info.vibrance; if (info.saturation !== undefined) desc.Strt = info.saturation; writeVersionAndDescriptor(writer, '', 'null', desc); }, ); function readHueChannel(reader: PsdReader): HueSaturationAdjustmentChannel { return { a: readInt16(reader), b: readInt16(reader), c: readInt16(reader), d: readInt16(reader), hue: readInt16(reader), saturation: readInt16(reader), lightness: readInt16(reader), }; } function writeHueChannel(writer: PsdWriter, channel: HueSaturationAdjustmentChannel | undefined) { const c = channel || {} as Partial; writeInt16(writer, c.a || 0); writeInt16(writer, c.b || 0); writeInt16(writer, c.c || 0); writeInt16(writer, c.d || 0); writeInt16(writer, c.hue || 0); writeInt16(writer, c.saturation || 0); writeInt16(writer, c.lightness || 0); } addHandler( 'hue2', adjustmentType('hue/saturation'), (reader, target, left) => { if (readUint16(reader) !== 2) throw new Error('Invalid hue2 version'); target.adjustment = { ...target.adjustment as PresetInfo, type: 'hue/saturation', master: readHueChannel(reader), reds: readHueChannel(reader), yellows: readHueChannel(reader), greens: readHueChannel(reader), cyans: readHueChannel(reader), blues: readHueChannel(reader), magentas: readHueChannel(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as HueSaturationAdjustment; writeUint16(writer, 2); // version writeHueChannel(writer, info.master); writeHueChannel(writer, info.reds); writeHueChannel(writer, info.yellows); writeHueChannel(writer, info.greens); writeHueChannel(writer, info.cyans); writeHueChannel(writer, info.blues); writeHueChannel(writer, info.magentas); }, ); function readColorBalance(reader: PsdReader): ColorBalanceValues { return { cyanRed: readInt16(reader), magentaGreen: readInt16(reader), yellowBlue: readInt16(reader), }; } function writeColorBalance(writer: PsdWriter, value: Partial) { writeInt16(writer, value.cyanRed || 0); writeInt16(writer, value.magentaGreen || 0); writeInt16(writer, value.yellowBlue || 0); } addHandler( 'blnc', adjustmentType('color balance'), (reader, target, left) => { target.adjustment = { type: 'color balance', shadows: readColorBalance(reader), midtones: readColorBalance(reader), highlights: readColorBalance(reader), preserveLuminosity: !!readUint8(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as ColorBalanceAdjustment; writeColorBalance(writer, info.shadows || {}); writeColorBalance(writer, info.midtones || {}); writeColorBalance(writer, info.highlights || {}); writeUint8(writer, info.preserveLuminosity ? 1 : 0); writeZeros(writer, 1); }, ); interface BlackAndWhiteDescriptor { 'Rd ': number; Yllw: number; 'Grn ': number; 'Cyn ': number; 'Bl ': number; Mgnt: number; useTint: boolean; tintColor?: DescriptorColor; bwPresetKind: number; blackAndWhitePresetFileName: string; } addHandler( 'blwh', adjustmentType('black & white'), (reader, target, left) => { const desc: BlackAndWhiteDescriptor = readVersionAndDescriptor(reader); target.adjustment = { type: 'black & white', reds: desc['Rd '], yellows: desc.Yllw, greens: desc['Grn '], cyans: desc['Cyn '], blues: desc['Bl '], magentas: desc.Mgnt, useTint: !!desc.useTint, presetKind: desc.bwPresetKind, presetFileName: desc.blackAndWhitePresetFileName, }; if (desc.tintColor !== undefined) target.adjustment.tintColor = parseColor(desc.tintColor); skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as BlackAndWhiteAdjustment; const desc: BlackAndWhiteDescriptor = { 'Rd ': info.reds || 0, Yllw: info.yellows || 0, 'Grn ': info.greens || 0, 'Cyn ': info.cyans || 0, 'Bl ': info.blues || 0, Mgnt: info.magentas || 0, useTint: !!info.useTint, tintColor: serializeColor(info.tintColor), bwPresetKind: info.presetKind || 0, blackAndWhitePresetFileName: info.presetFileName || '', }; writeVersionAndDescriptor(writer, '', 'null', desc); }, ); addHandler( 'phfl', adjustmentType('photo filter'), (reader, target, left) => { const version = readUint16(reader); if (version !== 2 && version !== 3) throw new Error('Invalid phfl version'); let color: Color; if (version === 2) { color = readColor(reader); } else { // version 3 // TODO: test this, this is probably wrong color = { l: readInt32(reader) / 100, a: readInt32(reader) / 100, b: readInt32(reader) / 100, }; } target.adjustment = { type: 'photo filter', color, density: readUint32(reader) / 100, preserveLuminosity: !!readUint8(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as PhotoFilterAdjustment; writeUint16(writer, 2); // version writeColor(writer, info.color || { l: 0, a: 0, b: 0 }); writeUint32(writer, (info.density || 0) * 100); writeUint8(writer, info.preserveLuminosity ? 1 : 0); writeZeros(writer, 3); }, ); function readMixrChannel(reader: PsdReader): ChannelMixerChannel { const red = readInt16(reader); const green = readInt16(reader); const blue = readInt16(reader); skipBytes(reader, 2); const constant = readInt16(reader); return { red, green, blue, constant }; } function writeMixrChannel(writer: PsdWriter, channel: ChannelMixerChannel | undefined) { const c = channel || {} as Partial; writeInt16(writer, c.red!); writeInt16(writer, c.green!); writeInt16(writer, c.blue!); writeZeros(writer, 2); writeInt16(writer, c.constant!); } addHandler( 'mixr', adjustmentType('channel mixer'), (reader, target, left) => { if (readUint16(reader) !== 1) throw new Error('Invalid mixr version'); const adjustment: ChannelMixerAdjustment = target.adjustment = { ...target.adjustment as PresetInfo, type: 'channel mixer', monochrome: !!readUint16(reader), }; if (!adjustment.monochrome) { adjustment.red = readMixrChannel(reader); adjustment.green = readMixrChannel(reader); adjustment.blue = readMixrChannel(reader); } adjustment.gray = readMixrChannel(reader); skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as ChannelMixerAdjustment; writeUint16(writer, 1); // version writeUint16(writer, info.monochrome ? 1 : 0); if (info.monochrome) { writeMixrChannel(writer, info.gray); writeZeros(writer, 3 * 5 * 2); } else { writeMixrChannel(writer, info.red); writeMixrChannel(writer, info.green); writeMixrChannel(writer, info.blue); writeMixrChannel(writer, info.gray); } }, ); const colorLookupType = createEnum<'3dlut' | 'abstractProfile' | 'deviceLinkProfile'>('colorLookupType', '3DLUT', { '3dlut': '3DLUT', abstractProfile: 'abstractProfile', deviceLinkProfile: 'deviceLinkProfile', }); const LUTFormatType = createEnum<'look' | 'cube' | '3dl'>('LUTFormatType', 'look', { look: 'LUTFormatLOOK', cube: 'LUTFormatCUBE', '3dl': 'LUTFormat3DL', }); const colorLookupOrder = createEnum<'rgb' | 'bgr'>('colorLookupOrder', 'rgb', { rgb: 'rgbOrder', bgr: 'bgrOrder', }); interface ColorLookupDescriptor { lookupType?: string; 'Nm '?: string; Dthr?: boolean; profile?: Uint8Array; LUTFormat?: string; dataOrder?: string; tableOrder?: string; LUT3DFileData?: Uint8Array; LUT3DFileName?: string; } addHandler( 'clrL', adjustmentType('color lookup'), (reader, target, left) => { if (readUint16(reader) !== 1) throw new Error('Invalid clrL version'); const desc: ColorLookupDescriptor = readVersionAndDescriptor(reader); target.adjustment = { type: 'color lookup' }; const info = target.adjustment; if (desc.lookupType !== undefined) info.lookupType = colorLookupType.decode(desc.lookupType); if (desc['Nm '] !== undefined) info.name = desc['Nm ']; if (desc.Dthr !== undefined) info.dither = desc.Dthr; if (desc.profile !== undefined) info.profile = desc.profile; if (desc.LUTFormat !== undefined) info.lutFormat = LUTFormatType.decode(desc.LUTFormat); if (desc.dataOrder !== undefined) info.dataOrder = colorLookupOrder.decode(desc.dataOrder); if (desc.tableOrder !== undefined) info.tableOrder = colorLookupOrder.decode(desc.tableOrder); if (desc.LUT3DFileData !== undefined) info.lut3DFileData = desc.LUT3DFileData; if (desc.LUT3DFileName !== undefined) info.lut3DFileName = desc.LUT3DFileName; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as ColorLookupAdjustment; const desc: ColorLookupDescriptor = {}; if (info.lookupType !== undefined) desc.lookupType = colorLookupType.encode(info.lookupType); if (info.name !== undefined) desc['Nm '] = info.name; if (info.dither !== undefined) desc.Dthr = info.dither; if (info.profile !== undefined) desc.profile = info.profile; if (info.lutFormat !== undefined) desc.LUTFormat = LUTFormatType.encode(info.lutFormat); if (info.dataOrder !== undefined) desc.dataOrder = colorLookupOrder.encode(info.dataOrder); if (info.tableOrder !== undefined) desc.tableOrder = colorLookupOrder.encode(info.tableOrder); if (info.lut3DFileData !== undefined) desc.LUT3DFileData = info.lut3DFileData; if (info.lut3DFileName !== undefined) desc.LUT3DFileName = info.lut3DFileName; writeUint16(writer, 1); // version writeVersionAndDescriptor(writer, '', 'null', desc); }, ); addHandler( 'nvrt', adjustmentType('invert'), (reader, target, left) => { target.adjustment = { type: 'invert' }; skipBytes(reader, left()); }, () => { // nothing to write here }, ); addHandler( 'post', adjustmentType('posterize'), (reader, target, left) => { target.adjustment = { type: 'posterize', levels: readUint16(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as PosterizeAdjustment; writeUint16(writer, info.levels ?? 4); writeZeros(writer, 2); }, ); addHandler( 'thrs', adjustmentType('threshold'), (reader, target, left) => { target.adjustment = { type: 'threshold', level: readUint16(reader), }; skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment as ThresholdAdjustment; writeUint16(writer, info.level ?? 128); writeZeros(writer, 2); }, ); const grdmColorModels = ['', '', '', 'rgb', 'hsb', '', 'lab']; addHandler( 'grdm', adjustmentType('gradient map'), (reader, target, left) => { const version = readUint16(reader); if (version !== 1 && version !== 3) throw new Error('Invalid grdm version'); const info: GradientMapAdjustment = { type: 'gradient map', gradientType: 'solid', }; info.reverse = !!readUint8(reader); info.dither = !!readUint8(reader); const hasMethod = !!readUint8(reader); reader.offset--; if (hasMethod) { const method = readSignature(reader); info.method = gradientInterpolationMethodType.decode(method); } info.name = readUnicodeString(reader); info.colorStops = []; info.opacityStops = []; const stopsCount = readUint16(reader); for (let i = 0; i < stopsCount; i++) { info.colorStops.push({ location: readUint32(reader), midpoint: readUint32(reader) / 100, color: readColor(reader), }); skipBytes(reader, 2); } const opacityStopsCount = readUint16(reader); for (let i = 0; i < opacityStopsCount; i++) { info.opacityStops.push({ location: readUint32(reader), midpoint: readUint32(reader) / 100, opacity: readUint16(reader) / 0xff, }); } const expansionCount = readUint16(reader); if (expansionCount !== 2) throw new Error('Invalid grdm expansion count'); const interpolation = readUint16(reader); info.smoothness = interpolation / 4096; const length = readUint16(reader); if (length !== 32) throw new Error('Invalid grdm length'); info.gradientType = readUint16(reader) ? 'noise' : 'solid'; info.randomSeed = readUint32(reader); info.addTransparency = !!readUint16(reader); info.restrictColors = !!readUint16(reader); info.roughness = readUint32(reader) / 4096; info.colorModel = (grdmColorModels[readUint16(reader)] || 'rgb') as 'rgb' | 'hsb' | 'lab'; info.min = [ readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, ]; info.max = [ readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, readUint16(reader) / 0x8000, ]; skipBytes(reader, left()); for (const s of info.colorStops) s.location /= interpolation; for (const s of info.opacityStops) s.location /= interpolation; target.adjustment = info; }, (writer, target) => { const info = target.adjustment as GradientMapAdjustment; writeUint16(writer, info.method !== undefined ? 3 : 1); // version writeUint8(writer, info.reverse ? 1 : 0); writeUint8(writer, info.dither ? 1 : 0); if (info.method !== undefined) { writeSignature(writer, gradientInterpolationMethodType.encode(info.method)); } writeUnicodeStringWithPadding(writer, info.name || ''); writeUint16(writer, info.colorStops && info.colorStops.length || 0); const interpolation = Math.round((info.smoothness ?? 1) * 4096); for (const s of info.colorStops || []) { writeUint32(writer, Math.round(s.location * interpolation)); writeUint32(writer, Math.round(s.midpoint * 100)); writeColor(writer, s.color); writeZeros(writer, 2); } writeUint16(writer, info.opacityStops && info.opacityStops.length || 0); for (const s of info.opacityStops || []) { writeUint32(writer, Math.round(s.location * interpolation)); writeUint32(writer, Math.round(s.midpoint * 100)); writeUint16(writer, Math.round(s.opacity * 0xff)); } writeUint16(writer, 2); // expansion count writeUint16(writer, interpolation); writeUint16(writer, 32); // length writeUint16(writer, info.gradientType === 'noise' ? 1 : 0); writeUint32(writer, info.randomSeed || 0); writeUint16(writer, info.addTransparency ? 1 : 0); writeUint16(writer, info.restrictColors ? 1 : 0); writeUint32(writer, Math.round((info.roughness ?? 1) * 4096)); const colorModel = grdmColorModels.indexOf(info.colorModel ?? 'rgb'); writeUint16(writer, colorModel === -1 ? 3 : colorModel); for (let i = 0; i < 4; i++) writeUint16(writer, Math.round((info.min && info.min[i] || 0) * 0x8000)); for (let i = 0; i < 4; i++) writeUint16(writer, Math.round((info.max && info.max[i] || 0) * 0x8000)); writeZeros(writer, 4); }, ); function readSelectiveColors(reader: PsdReader): CMYK { return { c: readInt16(reader), m: readInt16(reader), y: readInt16(reader), k: readInt16(reader), }; } function writeSelectiveColors(writer: PsdWriter, cmyk: CMYK | undefined) { const c = cmyk || {} as Partial; writeInt16(writer, c.c!); writeInt16(writer, c.m!); writeInt16(writer, c.y!); writeInt16(writer, c.k!); } addHandler( 'selc', adjustmentType('selective color'), (reader, target) => { if (readUint16(reader) !== 1) throw new Error('Invalid selc version'); const mode = readUint16(reader) ? 'absolute' : 'relative'; skipBytes(reader, 8); target.adjustment = { type: 'selective color', mode, reds: readSelectiveColors(reader), yellows: readSelectiveColors(reader), greens: readSelectiveColors(reader), cyans: readSelectiveColors(reader), blues: readSelectiveColors(reader), magentas: readSelectiveColors(reader), whites: readSelectiveColors(reader), neutrals: readSelectiveColors(reader), blacks: readSelectiveColors(reader), }; }, (writer, target) => { const info = target.adjustment as SelectiveColorAdjustment; writeUint16(writer, 1); // version writeUint16(writer, info.mode === 'absolute' ? 1 : 0); writeZeros(writer, 8); writeSelectiveColors(writer, info.reds); writeSelectiveColors(writer, info.yellows); writeSelectiveColors(writer, info.greens); writeSelectiveColors(writer, info.cyans); writeSelectiveColors(writer, info.blues); writeSelectiveColors(writer, info.magentas); writeSelectiveColors(writer, info.whites); writeSelectiveColors(writer, info.neutrals); writeSelectiveColors(writer, info.blacks); }, ); interface BrightnessContrastDescriptor { Vrsn: number; Brgh: number; Cntr: number; means: number; 'Lab ': boolean; useLegacy: boolean; Auto: boolean; } interface PresetDescriptor { Vrsn: number; presetKind: number; presetFileName: string; } interface CurvesPresetDescriptor { Vrsn: number; curvesPresetKind: number; curvesPresetFileName: string; } interface MixerPresetDescriptor { Vrsn: number; mixerPresetKind: number; mixerPresetFileName: string; } addHandler( 'CgEd', target => { const a = target.adjustment; if (!a) return false; return (a.type === 'brightness/contrast' && !a.useLegacy) || ((a.type === 'levels' || a.type === 'curves' || a.type === 'exposure' || a.type === 'channel mixer' || a.type === 'hue/saturation') && a.presetFileName !== undefined); }, (reader, target, left) => { const desc = readVersionAndDescriptor(reader) as BrightnessContrastDescriptor | PresetDescriptor | CurvesPresetDescriptor | MixerPresetDescriptor; if (desc.Vrsn !== 1) throw new Error('Invalid CgEd version'); // this section can specify preset file name for other adjustment types if ('presetFileName' in desc) { target.adjustment = { ...target.adjustment as LevelsAdjustment | ExposureAdjustment | HueSaturationAdjustment, presetKind: desc.presetKind, presetFileName: desc.presetFileName, }; } else if ('curvesPresetFileName' in desc) { target.adjustment = { ...target.adjustment as CurvesAdjustment, presetKind: desc.curvesPresetKind, presetFileName: desc.curvesPresetFileName, }; } else if ('mixerPresetFileName' in desc) { target.adjustment = { ...target.adjustment as CurvesAdjustment, presetKind: desc.mixerPresetKind, presetFileName: desc.mixerPresetFileName, }; } else { target.adjustment = { type: 'brightness/contrast', brightness: desc.Brgh, contrast: desc.Cntr, meanValue: desc.means, useLegacy: !!desc.useLegacy, labColorOnly: !!desc['Lab '], auto: !!desc.Auto, }; } skipBytes(reader, left()); }, (writer, target) => { const info = target.adjustment!; if (info.type === 'levels' || info.type === 'exposure' || info.type === 'hue/saturation') { const desc: PresetDescriptor = { Vrsn: 1, presetKind: info.presetKind ?? 1, presetFileName: info.presetFileName || '', }; writeVersionAndDescriptor(writer, '', 'null', desc); } else if (info.type === 'curves') { const desc: CurvesPresetDescriptor = { Vrsn: 1, curvesPresetKind: info.presetKind ?? 1, curvesPresetFileName: info.presetFileName || '', }; writeVersionAndDescriptor(writer, '', 'null', desc); } else if (info.type === 'channel mixer') { const desc: MixerPresetDescriptor = { Vrsn: 1, mixerPresetKind: info.presetKind ?? 1, mixerPresetFileName: info.presetFileName || '', }; writeVersionAndDescriptor(writer, '', 'null', desc); } else if (info.type === 'brightness/contrast') { const desc: BrightnessContrastDescriptor = { Vrsn: 1, Brgh: info.brightness || 0, Cntr: info.contrast || 0, means: info.meanValue ?? 127, 'Lab ': !!info.labColorOnly, useLegacy: !!info.useLegacy, Auto: !!info.auto, }; writeVersionAndDescriptor(writer, '', 'null', desc); } else { throw new Error('Unhandled CgEd case'); } }, ); function getTextLayersSortedByIndex(psd: Psd) { const layers: (Layer | undefined)[] = []; function collect(layer: Layer | Psd) { if (layer.children) { for (const child of layer.children) { if (child.text?.index !== undefined) { layers[child.text.index] = child; } collect(child); } } } collect(psd); return layers; } addHandler( 'Txt2', hasKey('engineData'), (reader, target, left, psd) => { const data = readBytes(reader, left()); target.engineData = fromByteArray(data); const layersByIndex = getTextLayersSortedByIndex(psd); const engineData = parseEngineData(data); const engineData2 = decodeEngineData2(engineData); const TextFrameSet = engineData2.ResourceDict.TextFrameSet; if (TextFrameSet) { for (let i = 0; i < TextFrameSet.length; i++) { const layer = layersByIndex[i]; if (TextFrameSet[i].path && layer?.text) { layer.text.textPath = TextFrameSet[i].path; } } } // console.log(require('util').inspect(engineData, false, 99, true)); // require('fs').writeFileSync('test_data.bin', data); // require('fs').writeFileSync('test_data.txt', require('util').inspect(engineData, false, 99, false), 'utf8'); // require('fs').writeFileSync('test_data.json', JSON.stringify(engineData2, null, 2), 'utf8'); }, (writer, target) => { const buffer = toByteArray(target.engineData!); writeBytes(writer, buffer); }, ); addHandler( 'FEid', hasKey('filterEffectsMasks'), (reader, target, leftBytes) => { const version = readInt32(reader); if (version < 1 || version > 3) throw new Error(`Invalid filterEffects version ${version}`); target.filterEffectsMasks = []; while (leftBytes() > 8) { if (readUint32(reader)) throw new Error('filterEffects: 64 bit length is not supported'); const length = readUint32(reader); const end = reader.offset + length; const id = readPascalString(reader, 1); const effectVersion = readInt32(reader); if (effectVersion !== 1) throw new Error(`Invalid filterEffect version ${effectVersion}`); if (readUint32(reader)) throw new Error('filterEffect: 64 bit length is not supported'); /*const effectLength =*/ readUint32(reader); // const endOfEffect = reader.offset + effectLength; const top = readInt32(reader); const left = readInt32(reader); const bottom = readInt32(reader); const right = readInt32(reader); const depth = readInt32(reader); const maxChannels = readInt32(reader); const channels: ({ compressionMode: number; data: Uint8Array; } | undefined)[] = []; // 0 -> R, 1 -> G, 2 -> B, 25 -> A for (let i = 0; i < (maxChannels + 2); i++) { // channels + user mask + sheet mask const exists = readInt32(reader); if (exists) { if (readUint32(reader)) throw new Error('filterEffect: 64 bit length is not supported'); const channelLength = readUint32(reader); if (!channelLength) throw new Error('filterEffect: Empty channel'); const compressionMode = readUint16(reader); const data = readBytes(reader, channelLength - 2); channels.push({ compressionMode, data }); } else { channels.push(undefined); } } target.filterEffectsMasks.push({ id, top, left, bottom, right, depth, channels }); if (reader.offset < end && readUint8(reader)) { const top = readInt32(reader); const left = readInt32(reader); const bottom = readInt32(reader); const right = readInt32(reader); if (readUint32(reader)) throw new Error('filterEffect: 64 bit length is not supported'); const extraLength = readUint32(reader); const compressionMode = readUint16(reader); const data = readBytes(reader, extraLength - 2); target.filterEffectsMasks[target.filterEffectsMasks.length - 1].extra = { top, left, bottom, right, compressionMode, data }; } reader.offset = end; let len = length; while (len % 4) { reader.offset++; len++; } } }, (writer, target) => { writeInt32(writer, 3); // version for (const mask of target.filterEffectsMasks!) { writeUint32(writer, 0); writeUint32(writer, 0); const lengthOffset = writer.offset; writePascalString(writer, mask.id, 1); writeInt32(writer, 1); // version writeUint32(writer, 0); writeUint32(writer, 0); const length2Offset = writer.offset; writeInt32(writer, mask.top); writeInt32(writer, mask.left); writeInt32(writer, mask.bottom); writeInt32(writer, mask.right); writeInt32(writer, mask.depth); const maxChannels = Math.max(0, mask.channels.length - 2); writeInt32(writer, maxChannels); for (let i = 0; i < (maxChannels + 2); i++) { const channel = mask.channels[i]; writeInt32(writer, channel ? 1 : 0); if (channel) { writeUint32(writer, 0); writeUint32(writer, channel.data.length + 2); writeUint16(writer, channel.compressionMode); writeBytes(writer, channel.data); } } writer.view.setUint32(length2Offset - 4, writer.offset - length2Offset, false); const extra = target.filterEffectsMasks![target.filterEffectsMasks!.length - 1]?.extra; if (extra) { writeUint8(writer, 1); writeInt32(writer, extra.top); writeInt32(writer, extra.left); writeInt32(writer, extra.bottom); writeInt32(writer, extra.right); writeUint32(writer, 0); writeUint32(writer, extra.data.byteLength + 2); writeUint16(writer, extra.compressionMode); writeBytes(writer, extra.data); } let length = writer.offset - lengthOffset; writer.view.setUint32(lengthOffset - 4, length, false); while (length % 4) { writeZeros(writer, 1); length++; } } }, ); addHandlerAlias('FXid', 'FEid'); addHandler( 'FMsk', hasKey('filterMask'), (reader, target) => { target.filterMask = { colorSpace: readColor(reader), opacity: readUint16(reader) / 0xff, }; }, (writer, target) => { writeColor(writer, target.filterMask!.colorSpace); writeUint16(writer, clamp(target.filterMask!.opacity ?? 1, 0, 1) * 0xff); }, ); interface ArtdDescriptor { 'Cnt ': number; autoExpandOffset: { Hrzn: number; Vrtc: number; }; origin: { Hrzn: number; Vrtc: number; }; autoExpandEnabled: boolean; autoNestEnabled: boolean; autoPositionEnabled: boolean; shrinkwrapOnSaveEnabled?: boolean; docDefaultNewArtboardBackgroundColor: DescriptorColor; docDefaultNewArtboardBackgroundType: number; } addHandler( 'artd', // document-wide artboard info target => (target as Psd).artboards !== undefined, (reader, target, left) => { const desc = readVersionAndDescriptor(reader) as ArtdDescriptor; (target as Psd).artboards = { count: desc['Cnt '], autoExpandOffset: { horizontal: desc.autoExpandOffset.Hrzn, vertical: desc.autoExpandOffset.Vrtc }, origin: { horizontal: desc.origin.Hrzn, vertical: desc.origin.Vrtc }, autoExpandEnabled: desc.autoExpandEnabled, autoNestEnabled: desc.autoNestEnabled, autoPositionEnabled: desc.autoPositionEnabled, shrinkwrapOnSaveEnabled: !!desc.shrinkwrapOnSaveEnabled, docDefaultNewArtboardBackgroundColor: parseColor(desc.docDefaultNewArtboardBackgroundColor), docDefaultNewArtboardBackgroundType: desc.docDefaultNewArtboardBackgroundType, }; skipBytes(reader, left()); }, (writer, target) => { const artb = (target as Psd).artboards!; const desc: ArtdDescriptor = { 'Cnt ': artb.count, autoExpandOffset: artb.autoExpandOffset ? { Hrzn: artb.autoExpandOffset.horizontal, Vrtc: artb.autoExpandOffset.vertical } : { Hrzn: 0, Vrtc: 0 }, origin: artb.origin ? { Hrzn: artb.origin.horizontal, Vrtc: artb.origin.vertical } : { Hrzn: 0, Vrtc: 0 }, autoExpandEnabled: artb.autoExpandEnabled ?? true, autoNestEnabled: artb.autoNestEnabled ?? true, autoPositionEnabled: artb.autoPositionEnabled ?? true, shrinkwrapOnSaveEnabled: artb.shrinkwrapOnSaveEnabled ?? true, docDefaultNewArtboardBackgroundColor: serializeColor(artb.docDefaultNewArtboardBackgroundColor), docDefaultNewArtboardBackgroundType: artb.docDefaultNewArtboardBackgroundType ?? 1, }; writeVersionAndDescriptor(writer, '', 'null', desc, 'artd'); }, ); export function hasMultiEffects(effects: LayerEffectsInfo) { return Object.keys(effects).map(key => (effects as any)[key]).some(v => Array.isArray(v) && v.length > 1); } addHandler( 'lfx2', target => target.effects !== undefined && !hasMultiEffects(target.effects), (reader, target, left) => { const version = readUint32(reader); if (version !== 0) throw new Error(`Invalid lfx2 version`); const desc: Lfx2Descriptor & LmfxDescriptor = readVersionAndDescriptor(reader); // console.log('READ', require('util').inspect(desc, false, 99, true)); // TODO: don't discard if we got it from lmfx // discard if read in 'lrFX' section target.effects = parseEffects(desc, !!reader.logMissingFeatures); skipBytes(reader, left()); }, (writer, target, _, options) => { const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true); // console.log('WRITE', require('util').inspect(desc, false, 99, true)); writeUint32(writer, 0); // version writeVersionAndDescriptor(writer, '', 'null', desc); }, ); interface CinfDescriptor { Vrsn: { major: number; minor: number; fix: number; }; psVersion?: { major: number; minor: number; fix: number; }; description: string; reason: string; Engn: string; // 'Engn.compCore'; enableCompCore?: string; // 'enable.feature'; enableCompCoreGPU?: string; // 'enable.feature'; enableCompCoreThreads?: string; // 'enable.feature'; compCoreSupport?: string; // 'reason.supported'; compCoreGPUSupport?: string; // 'reason.featureDisabled'; } addHandler( 'cinf', hasKey('compositorUsed'), (reader, target, left) => { const desc = readVersionAndDescriptor(reader) as CinfDescriptor; // console.log(require('util').inspect(desc, false, 99, true)); function enumValue(desc: string): string { return desc.split('.')[1]; } target.compositorUsed = { description: desc.description, reason: desc.reason, engine: enumValue(desc.Engn)!, }; if (desc.Vrsn) target.compositorUsed.version = desc.Vrsn; if (desc.psVersion) target.compositorUsed.photoshopVersion = desc.psVersion; if (desc.enableCompCore) target.compositorUsed.enableCompCore = enumValue(desc.enableCompCore); if (desc.enableCompCoreGPU) target.compositorUsed.enableCompCoreGPU = enumValue(desc.enableCompCoreGPU); if (desc.enableCompCoreThreads) target.compositorUsed.enableCompCoreThreads = enumValue(desc.enableCompCoreThreads); if (desc.compCoreSupport) target.compositorUsed.compCoreSupport = enumValue(desc.compCoreSupport); if (desc.compCoreGPUSupport) target.compositorUsed.compCoreGPUSupport = enumValue(desc.compCoreGPUSupport); skipBytes(reader, left()); }, (writer, target) => { const cinf = target.compositorUsed!; const desc: CinfDescriptor = { Vrsn: cinf.version || { major: 1, minor: 0, fix: 0 }, } as any; if (cinf.photoshopVersion) desc.psVersion = cinf.photoshopVersion; desc.description = cinf.description; desc.reason = cinf.reason; desc.Engn = `Engn.${cinf.engine}`; if (cinf.enableCompCore) desc.enableCompCore = `enable.${cinf.enableCompCore}`; if (cinf.enableCompCoreGPU) desc.enableCompCoreGPU = `enable.${cinf.enableCompCoreGPU}`; if (cinf.enableCompCoreThreads) desc.enableCompCoreThreads = `enable.${cinf.enableCompCoreThreads}`; if (cinf.compCoreSupport) desc.compCoreSupport = `reason.${cinf.compCoreSupport}`; if (cinf.compCoreGPUSupport) desc.compCoreGPUSupport = `reason.${cinf.compCoreGPUSupport}`; writeVersionAndDescriptor(writer, '', 'null', desc); }, ); interface ExtensionDesc { generatorSettings: { generator_45_assets: { json: string; }; layerTime: number; }; } // extension settings ?, ignore it addHandler( 'extn', target => (target as any)._extn !== undefined, (reader, target) => { const desc: ExtensionDesc = readVersionAndDescriptor(reader); // console.log(require('util').inspect(desc, false, 99, true)); if (MOCK_HANDLERS) (target as any)._extn = desc; }, (writer, target) => { // TODO: need to add correct types for desc fields (resources/src.psd) if (MOCK_HANDLERS) writeVersionAndDescriptor(writer, '', 'null', (target as any)._extn); }, ); addHandler( 'iOpa', hasKey('fillOpacity'), (reader, target) => { target.fillOpacity = readUint8(reader) / 0xff; skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.fillOpacity! * 0xff); writeZeros(writer, 3); }, ); addHandler( 'brst', hasKey('channelBlendingRestrictions'), (reader, target, left) => { target.channelBlendingRestrictions = []; while (left() > 4) { target.channelBlendingRestrictions.push(readInt32(reader)); } }, (writer, target) => { for (const channel of target.channelBlendingRestrictions!) { writeInt32(writer, channel); } }, ); addHandler( 'tsly', hasKey('transparencyShapesLayer'), (reader, target) => { target.transparencyShapesLayer = !!readUint8(reader); skipBytes(reader, 3); }, (writer, target) => { writeUint8(writer, target.transparencyShapesLayer ? 1 : 0); writeZeros(writer, 3); }, );