From 02084d0955ecd12991d60015060a01e90975456a Mon Sep 17 00:00:00 2001 From: harry Date: Wed, 24 Jul 2024 14:47:22 +0700 Subject: [PATCH] feat: change read psd to async function, support callback for each layer data --- .gitignore | 1 + package-lock.json | 4 +- src/additionalInfo.ts | 9655 +++++++++++++++++++----------------- src/imageResources.ts | 2800 ++++++----- src/index.ts | 72 +- src/psd.ts | 3049 ++++++------ src/psdReader.ts | 2485 ++++++---- src/test/common.ts | 344 +- src/test/psdReader.spec.ts | 948 ++-- src/test/psdWriter.spec.ts | 998 ++-- 10 files changed, 11014 insertions(+), 9342 deletions(-) diff --git a/.gitignore b/.gitignore index 27d2c42..4794b13 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .vscode TODO .idea +.tool-versions diff --git a/package-lock.json b/package-lock.json index bdc9715..7118ec3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ag-psd", - "version": "20.1.2", + "version": "20.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ag-psd", - "version": "20.1.2", + "version": "20.2.1", "license": "MIT", "dependencies": { "@types/base64-js": "^1.3.0", diff --git a/src/additionalInfo.ts b/src/additionalInfo.ts index 12d4591..1676b25 100644 --- a/src/additionalInfo.ts +++ b/src/additionalInfo.ts @@ -1,1221 +1,1590 @@ -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 } from './psd'; -import { PsdReader, readSignature, readUnicodeString, skipBytes, readUint32, readUint8, readFloat64, readUint16, readBytes, readInt16, checkSignature, readFloat32, readFixedPointPath32, readSection, readColor, readInt32, readPascalString, readUnicodeStringWithLength, readAsciiString, readPattern, readLayerInfo, ReadOptionsExt } 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 } from './descriptor'; -import { serializeEngineData, parseEngineData } from './engineData'; -import { encodeEngineData, decodeEngineData } from './text'; +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, +} from "./psd"; +import { + PsdReader, + readSignature, + readUnicodeString, + skipBytes, + readUint32, + readUint8, + readFloat64, + readUint16, + readBytes, + readInt16, + checkSignature, + readFloat32, + readFixedPointPath32, + readSection, + readColor, + readInt32, + readPascalString, + readUnicodeStringWithLength, + readAsciiString, + readPattern, + readLayerInfo, + ReadOptionsExt, +} 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, +} from "./descriptor"; +import { serializeEngineData, parseEngineData } from "./engineData"; +import { encodeEngineData, decodeEngineData } from "./text"; export interface ExtendedWriteOptions extends WriteOptions { - layerIds: Set; - layerToId: Map; + layerIds: Set; + layerToId: Map; } type HasMethod = (target: LayerAdditionalInfo) => boolean; -type ReadMethod = (reader: PsdReader, target: LayerAdditionalInfo, left: () => number, psd: Psd, options: ReadOptionsExt) => void; -type WriteMethod = (writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) => void; +type ReadMethod = ( + reader: PsdReader, + target: LayerAdditionalInfo, + left: () => Promise, + psd: Psd, + options: ReadOptionsExt +) => Promise; +type WriteMethod = ( + writer: PsdWriter, + target: LayerAdditionalInfo, + psd: Psd, + options: ExtendedWriteOptions +) => void; export interface InfoHandler { - key: string; - has: HasMethod; - read: ReadMethod; - write: WriteMethod; + key: string; + has: HasMethod; + read: ReadMethod; + write: WriteMethod; } -const fromAtoZ = 'abcdefghijklmnopqrstuvwxyz'; +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; +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]; + infoHandlersMap[key] = infoHandlersMap[target]; } function hasKey(key: keyof LayerAdditionalInfo) { - return (target: LayerAdditionalInfo) => target[key] !== undefined; + 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); + 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); + 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); - }, + "TySh", + hasKey("text"), + async (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, await 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); - }, + "SoCo", + (target) => + target.vectorFill !== undefined && + target.vectorStroke === undefined && + target.vectorFill.type === "color", + async (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); - }, + "GdFl", + (target) => + target.vectorFill !== undefined && + target.vectorStroke === undefined && + (target.vectorFill.type === "solid" || target.vectorFill.type === "noise"), + async (reader, target, left) => { + const descriptor = readVersionAndDescriptor(reader); + target.vectorFill = parseVectorContent(descriptor); + skipBytes(reader, await 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); - }, + "PtFl", + (target) => + target.vectorFill !== undefined && + target.vectorStroke === undefined && + target.vectorFill.type === "pattern", + async (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); - }, + "vscg", + (target) => + target.vectorFill !== undefined && target.vectorStroke !== undefined, + async (reader, target, left) => { + readSignature(reader); // key + const desc = readVersionAndDescriptor(reader); + target.vectorFill = parseVectorContent(desc); + skipBytes(reader, await 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]; +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 +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); - // TODO: 'combine' here might be wrong - path = { - open: selector === 3, - operation: boolOp === -1 ? 'combine' : booleanOperations[boolOp], - knots: [], - fillRule: flags === 2 ? 'non-zero' : 'even-odd', - }; - 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; +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); + // TODO: 'combine' here might be wrong + path = { + open: selector === 3, + operation: boolOp === -1 ? "combine" : booleanOperations[boolOp], + knots: [], + fillRule: flags === 2 ? "non-zero" : "even-odd", + }; + 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); - } - - if (vectorMask.fillStartsWithAllPixels !== undefined) { - 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, Math.abs(booleanOperations.indexOf(path.operation))); // default to 1 if not found - 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); - } - } - }, + "vmsk", + hasKey("vectorMask"), + async (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, await left()); + + // drawBezierPaths(vectorMask.paths, width, height, 'out.png'); + + skipBytes(reader, await 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); + } + + if (vectorMask.fillStartsWithAllPixels !== undefined) { + 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, Math.abs(booleanOperations.indexOf(path.operation))); // default to 1 if not found + 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("vsms", "vmsk"); // addHandlerAlias('vmsk', 'vsms'); 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; - Left: DescriptorUnitsValue; - Btom: DescriptorUnitsValue; - Rght: DescriptorUnitsValue; - }; - 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; - }[]; + keyDescriptorList: { + keyShapeInvalidated?: boolean; + keyOriginType?: number; + keyOriginResolution?: number; + keyOriginRRectRadii?: { + unitValueQuadVersion: number; + topRight: DescriptorUnitsValue; + topLeft: DescriptorUnitsValue; + bottomLeft: DescriptorUnitsValue; + bottomRight: DescriptorUnitsValue; + }; + keyOriginShapeBBox?: { + unitValueQuadVersion: number; + "Top ": DescriptorUnitsValue; + Left: DescriptorUnitsValue; + Btom: DescriptorUnitsValue; + Rght: DescriptorUnitsValue; + }; + 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: parseUnits(i.keyOriginShapeBBox['Top ']), - left: parseUnits(i.keyOriginShapeBBox.Left), - bottom: parseUnits(i.keyOriginShapeBBox.Btom), - right: parseUnits(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.xy, 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]; - - if (item.keyShapeInvalidated) { - desc.keyDescriptorList.push({ keyShapeInvalidated: true, keyOriginIndex: i }); - } else { - 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], - }; - } - - out.keyOriginIndex = i; - } - } - - writeInt32(writer, 1); // version - writeVersionAndDescriptor(writer, '', 'null', desc); - } + "vogk", + hasKey("vectorOrigination"), + async (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: parseUnits(i.keyOriginShapeBBox["Top "]), + left: parseUnits(i.keyOriginShapeBBox.Left), + bottom: parseUnits(i.keyOriginShapeBBox.Btom), + right: parseUnits(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.xy, trnf.yy, trnf.tx, trnf.ty]; + } + + target.vectorOrigination.keyDescriptorList.push(item); + } + + skipBytes(reader, await 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]; + + if (item.keyShapeInvalidated) { + desc.keyDescriptorList.push({ + keyShapeInvalidated: true, + keyOriginIndex: i, + }); + } else { + 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], + }; + } + + out.keyOriginIndex = i; + } + } + + writeInt32(writer, 1); // version + writeVersionAndDescriptor(writer, "", "null", desc); + } ); addHandler( - 'lmfx', - target => target.effects !== undefined && hasMultiEffects(target.effects), - (reader, target, left, _, options) => { - const version = readUint32(reader); - if (version !== 0) throw new Error('Invalid lmfx version'); - - const desc: LmfxDescriptor = readVersionAndDescriptor(reader); - // console.log(require('util').inspect(info, false, 99, true)); - - // discard if read in 'lrFX' or 'lfx2' section - target.effects = parseEffects(desc, !!options.logMissingFeatures); - - skipBytes(reader, left()); - }, - (writer, target, _, options) => { - const desc = serializeEffects(target.effects!, !!options.logMissingFeatures, true); - - writeUint32(writer, 0); // version - writeVersionAndDescriptor(writer, '', 'null', desc); - }, + "lmfx", + (target) => target.effects !== undefined && hasMultiEffects(target.effects), + async (reader, target, left, _, options) => { + const version = readUint32(reader); + if (version !== 0) throw new Error("Invalid lmfx version"); + + const desc: LmfxDescriptor = readVersionAndDescriptor(reader); + // console.log(require('util').inspect(info, false, 99, true)); + + // discard if read in 'lrFX' or 'lfx2' section + target.effects = parseEffects(desc, !!options.logMissingFeatures); + + skipBytes(reader, await left()); + }, + (writer, target, _, options) => { + const desc = serializeEffects( + target.effects!, + !!options.logMissingFeatures, + 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!); - }, + "lrFX", + hasKey("effects"), + async (reader, target, left) => { + if (!target.effects) target.effects = readEffects(reader); + + skipBytes(reader, await left()); + }, + (writer, target) => { + writeEffects(writer, target.effects!); + } ); addHandler( - 'luni', - hasKey('name'), - (reader, target, left) => { - target.name = readUnicodeString(reader); - skipBytes(reader, left()); - }, - (writer, target) => { - writeUnicodeString(writer, target.name!); - // writeUint16(writer, 0); // padding (but not extending string length) - }, + "luni", + hasKey("name"), + async (reader, target, left) => { + target.name = readUnicodeString(reader); + skipBytes(reader, await 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!), + "lnsr", + hasKey("nameSource"), + async (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); - }, + "lyid", + hasKey("id"), + async (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); - } - } - }, + "lsct", + hasKey("sectionDivider"), + async (reader, target, left) => { + target.sectionDivider = { type: readUint32(reader) }; + + if (await left()) { + checkSignature(reader, "8BIM"); + target.sectionDivider.key = readSignature(reader); + } + + if (await 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'); +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); - }, + "clbl", + hasKey("blendClippendElements"), + async (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); - }, + "infx", + hasKey("blendInteriorElements"), + async (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); - }, + "knko", + hasKey("knockout"), + async (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); - }, + "lmgm", + hasKey("layerMaskAsGlobalMask"), + async (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); - }, + "lspf", + hasKey("protected"), + async (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); - }, + "lclr", + hasKey("layerColor"), + async (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; + 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[]; - }[]; + origFXRefPoint?: { Hrzn: number; Vrtc: number }; + LyrI: number; + layerSettings: { + enab?: boolean; + Ofst?: { Hrzn: number; Vrtc: number }; + FXRefPoint?: { Hrzn: number; Vrtc: number }; + compList: number[]; + }[]; } addHandler( - 'shmd', - target => target.timestamp !== undefined || target.animationFrames !== undefined || - target.animationFrameFlags !== undefined || target.timeline !== undefined || target.comps !== undefined, - (reader, target, left, _, options) => { - 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, !!options.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, !!options.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 { - options.logMissingFeatures && console.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); - } - }, + "shmd", + (target) => + target.timestamp !== undefined || + target.animationFrames !== undefined || + target.animationFrameFlags !== undefined || + target.timeline !== undefined || + target.comps !== undefined, + async (reader, target, left, _, options) => { + const count = readUint32(reader); + + for (let i = 0; i < count; i++) { + checkSignature(reader, "8BIM"); + const key = readSignature(reader); + readUint8(reader); // copy + skipBytes(reader, 3); + + await readSection(reader, 1, async (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, + !!options.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, + !!options.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 { + options.logMissingFeatures && + console.log('Unhandled "shmd" section key', key); + } + + skipBytes(reader, await left()); + }); + } + + skipBytes(reader, await 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( - '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); - }, + "vstk", + hasKey("vectorStroke"), + async (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, await 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; + 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); - }, + "artb", // per-layer arboard info + hasKey("artboard"), + async (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, await 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), + "sn2P", + hasKey("usingAlignedRendering"), + async (reader, target) => { + target.usingAlignedRendering = !!readUint32(reader); + }, + (writer, target) => writeUint32(writer, target.usingAlignedRendering ? 1 : 0) ); -const placedLayerTypes: PlacedLayerType[] = ['unknown', 'vector', 'raster', 'image stack']; +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; + 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; + 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: { - '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; + 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: { + "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); - }, + "PlLd", + hasKey("placedLayer"), + async (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, await 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; + _name: ""; + _classID: "Pnt "; + Hrzn: DescriptorUnitsValue; + Vrtc: DescriptorUnitsValue; } /* interface K3DLight { @@ -1260,296 +1629,331 @@ interface K3DLight { */ 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; - }; -} /*| { + _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[]; @@ -1571,229 +1975,248 @@ type SoLdDescriptorFilterItem = { 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; - 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: 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; + 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', @@ -1826,7 +2249,7 @@ type SoLdDescriptorFilterItem = { LnIs: DescriptorColor; LnNm: boolean; }; -}*//* | { +}*/ /* | { filterID: 2089; Fltr: { _name: 'Adaptive Wide Angle'; @@ -1842,7 +2265,7 @@ type SoLdDescriptorFilterItem = { imgX: number; imgY: number; }; -}*//* | { +}*/ /* | { filterID: 1195730531; Fltr: { _name: 'Filter Gallery'; @@ -1854,1576 +2277,1760 @@ type SoLdDescriptorFilterItem = { 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: 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; + }; + } +); interface SoLdDescriptorFilter { - _name: '', - _classID: 'filterFXStyle', - enab: boolean, - validAtPosition: boolean, - filterMaskEnable: boolean, - filterMaskLinked: boolean, - filterMaskExtendWithWhite: boolean, - filterFXList: SoLdDescriptorFilterItem[]; + _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); + 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); + 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); + return new Uint8Array(array.buffer, array.byteOffset, array.byteLength); } function arrayToPoints(array: number[] | Uint32Array | Float32Array) { - const points: { x: number; y: number }[] = []; + const points: { x: number; y: number }[] = []; - for (let i = 0; i < array.length; i += 2) { - points.push({ x: array[i], y: array[i + 1] }); - } + for (let i = 0; i < array.length; i += 2) { + points.push({ x: array[i], y: array[i + 1] }); + } - return points; + 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; + 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)); + return arrayToPoints(uint8ToFloat32(array)); } function hrznVrtcToPoint(desc: HrznVrtcDescriptor) { - return { - x: parseUnits(desc.Hrzn), - y: parseUnits(desc.Vrtc), - }; + 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 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): Filter { - 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), - }; - - switch (f.filterID) { - case 1098281575: return { ...base, type: 'average' }; - case 1114403360: return { ...base, type: 'blur' }; - case 1114403405: return { ...base, type: 'blur more' }; - case 697: return { - ...base, - type: 'box blur', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 1198747202: return { - ...base, - type: 'gaussian blur', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 1299476034: return { - ...base, - type: 'motion blur', - filter: { - angle: f.Fltr.Angl, - distance: parseUnits(f.Fltr.Dstn), - }, - }; - case 1382313026: return { - ...base, - type: 'radial blur', - filter: { - amount: f.Fltr.Amnt, - method: BlrM.decode(f.Fltr.BlrM), - quality: BlrQ.decode(f.Fltr.BlrQ), - }, - }; - case 702: return { - ...base, - type: 'shape blur', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - customShape: { name: f.Fltr.customShape['Nm '], id: f.Fltr.customShape.Idnt }, - }, - }; - case 1399681602: 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 701: return { - ...base, - type: 'surface blur', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - threshold: f.Fltr.Thsh, - }, - }; - case 1148416108: 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 1349411688: return { - ...base, - type: 'pinch', - filter: { - amount: f.Fltr.Amnt, - }, - }; - case 1349284384: return { - ...base, - type: 'polar coordinates', - filter: { - conversion: Cnvr.decode(f.Fltr.Cnvr), - }, - }; - case 1383099493: return { - ...base, - type: 'ripple', - filter: { - amount: f.Fltr.Amnt, - size: RplS.decode(f.Fltr.RplS), - }, - }; - case 1399353888: 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 1399875698: return { - ...base, - type: 'spherize', - filter: { - amount: f.Fltr.Amnt, - mode: SphM.decode(f.Fltr.SphM), - }, - }; - case 1417114220: return { - ...base, - type: 'twirl', - filter: { - angle: f.Fltr.Angl, - }, - }; - case 1466005093: 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 1516722791: return { - ...base, - type: 'zigzag', - filter: { - amount: f.Fltr.Amnt, - ridges: f.Fltr.NmbR, - style: ZZTy.decode(f.Fltr.ZZTy), - }, - }; - case 1097092723: 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 1148416099: return { ...base, type: 'despeckle' }; - case 1148417107: return { - ...base, - type: 'dust and scratches', - filter: { - radius: f.Fltr['Rds '], - threshold: f.Fltr.Thsh, - }, - }; - case 1298427424: return { - ...base, - type: 'median', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 633: 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(i => Chnl.decode(i)), - amount: c.Amnt, - ...(c.EdgF ? { preserveDetails: c.EdgF } : {}), - })), - }, - }; - case 1131180616: 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 1131574132: return { - ...base, - type: 'crystallize', - filter: { - cellSize: f.Fltr.ClSz, - randomSeed: f.Fltr.FlRs, - }, - }; - case 1180922912: return { ...base, type: 'facet' }; - case 1181902701: return { ...base, type: 'fragment' }; - case 1299870830: return { - ...base, - type: 'mezzotint', - filter: { - type: MztT.decode(f.Fltr.MztT), - randomSeed: f.Fltr.FlRs, - }, - }; - case 1299407648: return { - ...base, - type: 'mosaic', - filter: { - cellSize: parseUnits(f.Fltr.ClSz), - }, - }; - case 1349416044: return { - ...base, - type: 'pointillize', - filter: { - cellSize: f.Fltr.ClSz, - randomSeed: f.Fltr.FlRs, - }, - }; - case 1131177075: return { - ...base, - type: 'clouds', - filter: { - randomSeed: f.Fltr.FlRs, - }, - }; - case 1147564611: return { - ...base, - type: 'difference clouds', - filter: { - randomSeed: f.Fltr.FlRs, - }, - }; - case 1180856947: return { - ...base, - type: 'fibers', - filter: { - variance: f.Fltr.Vrnc, - strength: f.Fltr.Strg, - randomSeed: f.Fltr.RndS, - }, - }; - case 1282306886: 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 1399353968: return { ...base, type: 'sharpen' }; - case 1399353925: return { ...base, type: 'sharpen edges' }; - case 1399353933: return { ...base, type: 'sharpen more' }; - case 698: 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 1433301837: return { - ...base, - type: 'unsharp mask', - filter: { - amount: parsePercent(f.Fltr.Amnt), - radius: parseUnits(f.Fltr['Rds ']), - threshold: f.Fltr.Thsh, - }, - }; - case 1147564832: return { - ...base, - type: 'diffuse', - filter: { - mode: DfsM.decode(f.Fltr['Md ']), - randomSeed: f.Fltr.FlRs, - }, - }; - case 1164796531: return { - ...base, - type: 'emboss', - filter: { - angle: f.Fltr.Angl, - height: f.Fltr.Hght, - amount: f.Fltr.Amnt, - }, - }; - case 1165522034: 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 1181639749: return { ...base, type: 'find edges' }; - case 1399616122: return { ...base, type: 'solarize' }; - case 1416393504: return { - ...base, - type: 'tiles', - filter: { - numberOfTiles: f.Fltr.TlNm, - maximumOffset: f.Fltr.TlOf, - fillEmptyAreaWith: FlCl.decode(f.Fltr.FlCl), - randomSeed: f.Fltr.FlRs, - }, - }; - case 1416782659: return { - ...base, - type: 'trace contour', - filter: { - level: f.Fltr['Lvl '], - edge: CntE.decode(f.Fltr['Edg ']), - }, - }; - case 1466852384: return { - ...base, - type: 'wind', - filter: { - method: WndM.decode(f.Fltr.WndM), - direction: Drct.decode(f.Fltr.Drct), - }, - }; - case 1148089458: return { - ...base, - type: 'de-interlace', - filter: { - eliminate: IntE.decode(f.Fltr.IntE), - newFieldsBy: IntC.decode(f.Fltr.IntC), - }, - }; - case 1314149187: return { ...base, type: 'ntsc colors' }; - case 1131639917: return { - ...base, - type: 'custom', - filter: { - scale: f.Fltr['Scl '], - offset: f.Fltr.Ofst, - matrix: f.Fltr.Mtrx, - }, - }; - case 1214736464: return { - ...base, - type: 'high pass', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 1299737888: return { - ...base, - type: 'maximum', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 1299082528: return { - ...base, - type: 'minimum', - filter: { - radius: parseUnits(f.Fltr['Rds ']), - }, - }; - case 1332114292: return { - ...base, - type: 'offset', - filter: { - horizontal: f.Fltr.Hrzn, - vertical: f.Fltr.Vrtc, - undefinedAreas: FlMd.decode(f.Fltr['Fl ']), - }, - }; - case 991: 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 1348620396: { - 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 1215521360: return { - ...base, - type: 'hsb/hsl', - filter: { - inputMode: ClrS.decode(f.Fltr.Inpt) as any, - rowOrder: ClrS.decode(f.Fltr.Otpt) as any, - }, - }; - case 1122: 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 1282492025: { - return { - ...base, - type: 'liquify', - filter: { - liquifyMesh: f.Fltr.LqMe, - }, - }; - } - default: - throw new Error(`Unknown filterID: ${(f as any).filterID}`); - } + 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), + }; + + switch (f.filterID) { + case 1098281575: + return { ...base, type: "average" }; + case 1114403360: + return { ...base, type: "blur" }; + case 1114403405: + return { ...base, type: "blur more" }; + case 697: + return { + ...base, + type: "box blur", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 1198747202: + return { + ...base, + type: "gaussian blur", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 1299476034: + return { + ...base, + type: "motion blur", + filter: { + angle: f.Fltr.Angl, + distance: parseUnits(f.Fltr.Dstn), + }, + }; + case 1382313026: + return { + ...base, + type: "radial blur", + filter: { + amount: f.Fltr.Amnt, + method: BlrM.decode(f.Fltr.BlrM), + quality: BlrQ.decode(f.Fltr.BlrQ), + }, + }; + case 702: + return { + ...base, + type: "shape blur", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + customShape: { + name: f.Fltr.customShape["Nm "], + id: f.Fltr.customShape.Idnt, + }, + }, + }; + case 1399681602: + 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 701: + return { + ...base, + type: "surface blur", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + threshold: f.Fltr.Thsh, + }, + }; + case 1148416108: + 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 1349411688: + return { + ...base, + type: "pinch", + filter: { + amount: f.Fltr.Amnt, + }, + }; + case 1349284384: + return { + ...base, + type: "polar coordinates", + filter: { + conversion: Cnvr.decode(f.Fltr.Cnvr), + }, + }; + case 1383099493: + return { + ...base, + type: "ripple", + filter: { + amount: f.Fltr.Amnt, + size: RplS.decode(f.Fltr.RplS), + }, + }; + case 1399353888: + 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 1399875698: + return { + ...base, + type: "spherize", + filter: { + amount: f.Fltr.Amnt, + mode: SphM.decode(f.Fltr.SphM), + }, + }; + case 1417114220: + return { + ...base, + type: "twirl", + filter: { + angle: f.Fltr.Angl, + }, + }; + case 1466005093: + 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 1516722791: + return { + ...base, + type: "zigzag", + filter: { + amount: f.Fltr.Amnt, + ridges: f.Fltr.NmbR, + style: ZZTy.decode(f.Fltr.ZZTy), + }, + }; + case 1097092723: + 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 1148416099: + return { ...base, type: "despeckle" }; + case 1148417107: + return { + ...base, + type: "dust and scratches", + filter: { + radius: f.Fltr["Rds "], + threshold: f.Fltr.Thsh, + }, + }; + case 1298427424: + return { + ...base, + type: "median", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 633: + 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((i) => Chnl.decode(i)), + amount: c.Amnt, + ...(c.EdgF ? { preserveDetails: c.EdgF } : {}), + })), + }, + }; + case 1131180616: + 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 1131574132: + return { + ...base, + type: "crystallize", + filter: { + cellSize: f.Fltr.ClSz, + randomSeed: f.Fltr.FlRs, + }, + }; + case 1180922912: + return { ...base, type: "facet" }; + case 1181902701: + return { ...base, type: "fragment" }; + case 1299870830: + return { + ...base, + type: "mezzotint", + filter: { + type: MztT.decode(f.Fltr.MztT), + randomSeed: f.Fltr.FlRs, + }, + }; + case 1299407648: + return { + ...base, + type: "mosaic", + filter: { + cellSize: parseUnits(f.Fltr.ClSz), + }, + }; + case 1349416044: + return { + ...base, + type: "pointillize", + filter: { + cellSize: f.Fltr.ClSz, + randomSeed: f.Fltr.FlRs, + }, + }; + case 1131177075: + return { + ...base, + type: "clouds", + filter: { + randomSeed: f.Fltr.FlRs, + }, + }; + case 1147564611: + return { + ...base, + type: "difference clouds", + filter: { + randomSeed: f.Fltr.FlRs, + }, + }; + case 1180856947: + return { + ...base, + type: "fibers", + filter: { + variance: f.Fltr.Vrnc, + strength: f.Fltr.Strg, + randomSeed: f.Fltr.RndS, + }, + }; + case 1282306886: + 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 1399353968: + return { ...base, type: "sharpen" }; + case 1399353925: + return { ...base, type: "sharpen edges" }; + case 1399353933: + return { ...base, type: "sharpen more" }; + case 698: + 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 1433301837: + return { + ...base, + type: "unsharp mask", + filter: { + amount: parsePercent(f.Fltr.Amnt), + radius: parseUnits(f.Fltr["Rds "]), + threshold: f.Fltr.Thsh, + }, + }; + case 1147564832: + return { + ...base, + type: "diffuse", + filter: { + mode: DfsM.decode(f.Fltr["Md "]), + randomSeed: f.Fltr.FlRs, + }, + }; + case 1164796531: + return { + ...base, + type: "emboss", + filter: { + angle: f.Fltr.Angl, + height: f.Fltr.Hght, + amount: f.Fltr.Amnt, + }, + }; + case 1165522034: + 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 1181639749: + return { ...base, type: "find edges" }; + case 1399616122: + return { ...base, type: "solarize" }; + case 1416393504: + return { + ...base, + type: "tiles", + filter: { + numberOfTiles: f.Fltr.TlNm, + maximumOffset: f.Fltr.TlOf, + fillEmptyAreaWith: FlCl.decode(f.Fltr.FlCl), + randomSeed: f.Fltr.FlRs, + }, + }; + case 1416782659: + return { + ...base, + type: "trace contour", + filter: { + level: f.Fltr["Lvl "], + edge: CntE.decode(f.Fltr["Edg "]), + }, + }; + case 1466852384: + return { + ...base, + type: "wind", + filter: { + method: WndM.decode(f.Fltr.WndM), + direction: Drct.decode(f.Fltr.Drct), + }, + }; + case 1148089458: + return { + ...base, + type: "de-interlace", + filter: { + eliminate: IntE.decode(f.Fltr.IntE), + newFieldsBy: IntC.decode(f.Fltr.IntC), + }, + }; + case 1314149187: + return { ...base, type: "ntsc colors" }; + case 1131639917: + return { + ...base, + type: "custom", + filter: { + scale: f.Fltr["Scl "], + offset: f.Fltr.Ofst, + matrix: f.Fltr.Mtrx, + }, + }; + case 1214736464: + return { + ...base, + type: "high pass", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 1299737888: + return { + ...base, + type: "maximum", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 1299082528: + return { + ...base, + type: "minimum", + filter: { + radius: parseUnits(f.Fltr["Rds "]), + }, + }; + case 1332114292: + return { + ...base, + type: "offset", + filter: { + horizontal: f.Fltr.Hrzn, + vertical: f.Fltr.Vrtc, + undefinedAreas: FlMd.decode(f.Fltr["Fl "]), + }, + }; + case 991: + 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 1348620396: { + 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 1215521360: + return { + ...base, + type: "hsb/hsl", + filter: { + inputMode: ClrS.decode(f.Fltr.Inpt) as any, + rowOrder: ClrS.decode(f.Fltr.Otpt) as any, + }, + }; + case 1122: + 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 1282492025: { + return { + ...base, + type: "liquify", + filter: { + liquifyMesh: f.Fltr.LqMe, + }, + }; + } + default: + throw new Error(`Unknown filterID: ${(f as any).filterID}`); + } } function parseFilterFX(desc: SoLdDescriptorFilter): PlacedLayerFilter { - return { - enabled: desc.enab, - validAtPosition: desc.validAtPosition, - maskEnabled: desc.filterMaskEnable, - maskLinked: desc.filterMaskLinked, - maskExtendWithWhite: desc.filterMaskExtendWithWhite, - list: desc.filterFXList.map(parseFilterFXItem), - }; + return { + enabled: desc.enab, + validAtPosition: desc.validAtPosition, + maskEnabled: desc.filterMaskEnable, + maskLinked: desc.filterMaskLinked, + maskExtendWithWhite: desc.filterMaskExtendWithWhite, + list: desc.filterFXList.map(parseFilterFXItem), + }; } -function uvRadius(t: { radius: UnitsValue; }) { - return unitsValue(t.radius, 'radius'); +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: '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 '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, - }; - default: throw new Error(`Unknow filter type: ${(f as any).type}`); - } + 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: "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 "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, + }; + 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; - compInfo?: { compID: number; originalCompID: number; }; - Impr?: {}; // ??? + 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; + compInfo?: { compID: number; originalCompID: number }; + Impr?: {}; // ??? } // 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 }, - ], - }, - }; + 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); - // 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); - - // 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)), - }; - } - - // console.log('write', require('util').inspect(desc.filterFX, false, 99, true)); /// - - // 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'); - }, + "SoLd", + hasKey("placedLayer"), + async (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); + // 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); + + // console.log('filter', require('util').inspect(target.placedLayer.filter, false, 99, true)); + + skipBytes(reader, await 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)), + }; + } + + // console.log('write', require('util').inspect(desc.filterFX, false, 99, true)); /// + + // 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'); +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); - }, + "fxrp", + hasKey("referencePoint"), + async (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, options) => { - readLayerInfo(reader, psd, options); - }, - (_writer, _target) => { - }, + "Lr16", + () => false, + async (reader, _target, _left, psd, options) => { + await readLayerInfo(reader, psd, options); + }, + (_writer, _target) => {} ); addHandler( - 'Lr32', - () => false, - (reader, _target, _left, psd, options) => { - readLayerInfo(reader, psd, options); - }, - (_writer, _target) => { - }, + "Lr32", + () => false, + async (reader, _target, _left, psd, options) => { + await readLayerInfo(reader, psd, options); + }, + (_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); - }, + "LMsk", + hasKey("userMask"), + async (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( - 'vowv', // appears with Lr16 section ? - _ => false, - (reader, target, left) => { - const value = readUint32(reader); // 2 ???? - reader; target; - console.log('vowv', { value }, left()); - }, - (_writer, _target) => { - // TODO: write - }, - ); + addHandler( + "vowv", // appears with Lr16 section ? + (_) => false, + async (reader, target, left) => { + const value = readUint32(reader); // 2 ???? + reader; + target; + console.log("vowv", { value }, await left()); + }, + (_writer, _target) => { + // TODO: write + } + ); } 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), - ); + addHandler( + "Patt", + (target) => (target as any)._Patt !== undefined, + async (reader, target, left) => { + // console.log('additional info: Patt'); + (target as any)._Patt = readBytes(reader, await 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) => { - }, - ); + addHandler( + "Patt", // TODO: handle also Pat2 & Pat3 + (target) => !target, + async (reader, target, left) => { + if (!(await left())) return; + + skipBytes(reader, await left()); + return; // not supported yet + target; + readPattern; + + // if (!target.patterns) target.patterns = []; + // target.patterns.push(readPattern(reader)); + // skipBytes(reader, await left()); + }, + (_writer, _target) => {} + ); } /* @@ -3440,7 +4047,7 @@ addHandler( const desc = readVersionAndDescriptor(reader) as CAIDesc; console.log('CAI', require('util').inspect(desc, false, 99, true)); console.log('CAI', { version }); - console.log('CAI left', readBytes(reader, left())); // 8 bytes left, all zeroes + console.log('CAI left', readBytes(reader, await left())); // 8 bytes left, all zeroes }, (_writer, _target) => { }, @@ -3465,1506 +4072,1706 @@ addHandler( */ 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 }; + 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); +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); - } - } + "Anno", + (target) => (target as Psd).annotations !== undefined, + async (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, await 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; }; + compInfo: { compID: number; originalCompID: number }; } addHandler( - 'lnk2', - (target: any) => !!(target as Psd).linkedFiles && (target as Psd).linkedFiles!.length > 0, - (reader, target, left, _, options) => { - const psd = target as Psd; - psd.linkedFiles = psd.linkedFiles || []; - - while (left() > 8) { - let size = readLength64(reader); // size - 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) : 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(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 (options.skipLinkedFilesData) file.data = undefined; - - psd.linkedFiles.push(file); - linkedFileDescriptor; - - while (size % 4) size++; - reader.offset = startOffset + size; - } - - skipBytes(reader, left()); // ? - }, - (writer, target) => { - const psd = target as Psd; - - for (const file of psd.linkedFiles!) { - let version = 2; - - if (file.assetLockedState != null) version = 7; - else if (file.assetModTime != null) version = 6; - else if (file.childDocumentID != null) version = 5; - // TODO: else if (file.time != null) version = 3; (only for liFE) - - writeUint32(writer, 0); - writeUint32(writer, 0); // size - const sizeOffset = writer.offset; - writeSignature(writer, 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 (file.data) writeBytes(writer, file.data); - else writeLength64(writer, 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); - } - } - }, + "lnk2", + (target: any) => + !!(target as Psd).linkedFiles && (target as Psd).linkedFiles!.length > 0, + async (reader, target, left, _, options) => { + const psd = target as Psd; + psd.linkedFiles = psd.linkedFiles || []; + + while ((await left()) > 8) { + let size = readLength64(reader); // size + 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) : 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( + 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 (options.skipLinkedFilesData) file.data = undefined; + + psd.linkedFiles.push(file); + linkedFileDescriptor; + + while (size % 4) size++; + reader.offset = startOffset + size; + } + + skipBytes(reader, await left()); // ? + }, + (writer, target) => { + const psd = target as Psd; + + for (const file of psd.linkedFiles!) { + let version = 2; + + if (file.assetLockedState != null) version = 7; + else if (file.assetModTime != null) version = 6; + else if (file.childDocumentID != null) version = 5; + // TODO: else if (file.time != null) version = 3; (only for liFE) + + writeUint32(writer, 0); + writeUint32(writer, 0); // size + const sizeOffset = writer.offset; + writeSignature(writer, 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 (file.data) writeBytes(writer, file.data); + else writeLength64(writer, 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); + } + } + } ); -addHandlerAlias('lnkD', 'lnk2'); -addHandlerAlias('lnk3', 'lnk2'); -addHandlerAlias('lnkE', 'lnk2'); +addHandlerAlias("lnkD", "lnk2"); +addHandlerAlias("lnk3", "lnk2"); +addHandlerAlias("lnkE", "lnk2"); interface PthsDescriptor { - pathList: { - _classID: 'pathInfoClass'; - pathUnicodeName: string; - pathSymmetryClass: { - _classID: 'pathSymmetryClass'; - pathSymmetryMode: string; // 'pathSymmetryModeEnum.pathSymmetryModeBasicPath' - }; - }[]; + 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); - }, + "pths", + hasKey("pathList"), + async (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!), + "lyvr", + hasKey("version"), + async (reader, target) => { + target.version = readUint32(reader); + }, + (writer, target) => writeUint32(writer, target.version!) ); function adjustmentType(type: string) { - return (target: LayerAdditionalInfo) => !!target.adjustment && target.adjustment.type === type; + 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); - }, + "brit", + adjustmentType("brightness/contrast"), + async (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, await 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 }; + 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)); +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); - }, + "levl", + adjustmentType("levels"), + async (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, await 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 = []; + 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 }); - } + for (let j = 0; j < nodes; j++) { + const output = readInt16(reader); + const input = readInt16(reader); + channel.push({ input, output }); + } - return channel; + return channel; } -function writeCurveChannel(writer: PsdWriter, channel: CurvesAdjustmentChannel) { - writeUint16(writer, channel.length); +function writeCurveChannel( + writer: PsdWriter, + channel: CurvesAdjustmentChannel +) { + writeUint16(writer, channel.length); - for (const n of channel) { - writeUint16(writer, n.output); - writeUint16(writer, n.input); - } + 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); } - - writeZeros(writer, 2); - }, + "curv", + adjustmentType("curves"), + async (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, await 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); + } + + writeZeros(writer, 2); + } ); 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); - }, + "expA", + adjustmentType("exposure"), + async (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, await 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; + 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); - }, + "vibA", + adjustmentType("vibrance"), + async (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, await 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), - }; + 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); +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); - }, + "hue2", + adjustmentType("hue/saturation"), + async (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, await 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), - }; + 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); +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); - }, + "blnc", + adjustmentType("color balance"), + async (reader, target, left) => { + target.adjustment = { + type: "color balance", + shadows: readColorBalance(reader), + midtones: readColorBalance(reader), + highlights: readColorBalance(reader), + preserveLuminosity: !!readUint8(reader), + }; + + skipBytes(reader, await 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; + "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); - }, + "blwh", + adjustmentType("black & white"), + async (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, await 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); - }, + "phfl", + adjustmentType("photo filter"), + async (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, await 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 }; + 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!); +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); - } - }, + "mixr", + adjustmentType("channel mixer"), + async (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, await 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 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 LUTFormatType = createEnum<"look" | "cube" | "3dl">( + "LUTFormatType", + "look", + { + look: "LUTFormatLOOK", + cube: "LUTFormatCUBE", + "3dl": "LUTFormat3DL", + } +); -const colorLookupOrder = createEnum<'rgb' | 'bgr'>('colorLookupOrder', 'rgb', { - rgb: 'rgbOrder', - bgr: 'bgrOrder', +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; + 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); - }, + "clrL", + adjustmentType("color lookup"), + async (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, await 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 - }, + "nvrt", + adjustmentType("invert"), + async (reader, target, left) => { + target.adjustment = { type: "invert" }; + skipBytes(reader, await 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); - }, + "post", + adjustmentType("posterize"), + async (reader, target, left) => { + target.adjustment = { + type: "posterize", + levels: readUint16(reader), + }; + skipBytes(reader, await 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); - }, + "thrs", + adjustmentType("threshold"), + async (reader, target, left) => { + target.adjustment = { + type: "threshold", + level: readUint16(reader), + }; + skipBytes(reader, await left()); + }, + (writer, target) => { + const info = target.adjustment as ThresholdAdjustment; + writeUint16(writer, info.level ?? 128); + writeZeros(writer, 2); + } ); -const grdmColorModels = ['', '', '', 'rgb', 'hsb', '', 'lab']; +const grdmColorModels = ["", "", "", "rgb", "hsb", "", "lab"]; addHandler( - 'grdm', - adjustmentType('gradient map'), - (reader, target, left) => { - if (readUint16(reader) !== 1) throw new Error('Invalid grdm version'); - - const info: GradientMapAdjustment = { - type: 'gradient map', - gradientType: 'solid', - }; - - info.reverse = !!readUint8(reader); - info.dither = !!readUint8(reader); - 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, 1); // version - writeUint8(writer, info.reverse ? 1 : 0); - writeUint8(writer, info.dither ? 1 : 0); - 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); - }, + "grdm", + adjustmentType("gradient map"), + async (reader, target, left) => { + if (readUint16(reader) !== 1) throw new Error("Invalid grdm version"); + + const info: GradientMapAdjustment = { + type: "gradient map", + gradientType: "solid", + }; + + info.reverse = !!readUint8(reader); + info.dither = !!readUint8(reader); + 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, await 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, 1); // version + writeUint8(writer, info.reverse ? 1 : 0); + writeUint8(writer, info.dither ? 1 : 0); + 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), - }; + 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!); + 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); - }, + "selc", + adjustmentType("selective color"), + async (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; + Vrsn: number; + Brgh: number; + Cntr: number; + means: number; + "Lab ": boolean; + useLegacy: boolean; + Auto: boolean; } interface PresetDescriptor { - Vrsn: number; - presetKind: number; - presetFileName: string; + Vrsn: number; + presetKind: number; + presetFileName: string; } interface CurvesPresetDescriptor { - Vrsn: number; - curvesPresetKind: number; - curvesPresetFileName: string; + Vrsn: number; + curvesPresetKind: number; + curvesPresetFileName: string; } interface MixerPresetDescriptor { - Vrsn: number; - mixerPresetKind: number; - mixerPresetFileName: string; + 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'); - } - }, + "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) + ); + }, + async (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, await 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"); + } + } ); addHandler( - 'Txt2', - hasKey('engineData'), - (reader, target, left) => { - const data = readBytes(reader, left()); - target.engineData = fromByteArray(data); - // const engineData = parseEngineData(data); - // const engineData2 = decodeEngineData2(engineData); - // 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); - }, + "Txt2", + hasKey("engineData"), + async (reader, target, left) => { + const data = readBytes(reader, await left()); + target.engineData = fromByteArray(data); + // const engineData = parseEngineData(data); + // const engineData2 = decodeEngineData2(engineData); + // 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}`); - - if (readUint32(reader)) throw new Error('filterEffects: 64 bit length is not supported'); - const length = readUint32(reader); - const end = reader.offset + length; - target.filterEffectsMasks = []; - - while (reader.offset < end) { - 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++) { - const exists = readInt32(reader); - if (exists) { - if (readUint32(reader)) throw new Error('filterEffect: 64 bit length is not supported'); - const channelLength = readUint32(reader); - 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 (leftBytes() && readUint8(reader)) { - const compressionMode = readUint16(reader); - const data = readBytes(reader, leftBytes()); - target.filterEffectsMasks[target.filterEffectsMasks.length - 1].extra = { compressionMode, data }; - } - } - }, - (writer, target) => { - writeInt32(writer, 3); - writeUint32(writer, 0); - writeUint32(writer, 0); - const lengthOffset = writer.offset; - - for (const mask of target.filterEffectsMasks!) { - writePascalString(writer, mask.id, 1); - writeInt32(writer, 1); - 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); - writeUint16(writer, extra.compressionMode); - writeBytes(writer, extra.data); - } - - writer.view.setUint32(lengthOffset - 4, writer.offset - lengthOffset, false); - }, + "FEid", + hasKey("filterEffectsMasks"), + async (reader, target, leftBytes) => { + const version = readInt32(reader); + if (version < 1 || version > 3) + throw new Error(`Invalid filterEffects version ${version}`); + + if (readUint32(reader)) + throw new Error("filterEffects: 64 bit length is not supported"); + const length = readUint32(reader); + const end = reader.offset + length; + target.filterEffectsMasks = []; + + while (reader.offset < end) { + 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++) { + const exists = readInt32(reader); + if (exists) { + if (readUint32(reader)) + throw new Error("filterEffect: 64 bit length is not supported"); + const channelLength = readUint32(reader); + 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 ((await leftBytes()) && readUint8(reader)) { + const compressionMode = readUint16(reader); + const data = readBytes(reader, await leftBytes()); + target.filterEffectsMasks[target.filterEffectsMasks.length - 1].extra = + { compressionMode, data }; + } + } + }, + (writer, target) => { + writeInt32(writer, 3); + writeUint32(writer, 0); + writeUint32(writer, 0); + const lengthOffset = writer.offset; + + for (const mask of target.filterEffectsMasks!) { + writePascalString(writer, mask.id, 1); + writeInt32(writer, 1); + 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); + writeUint16(writer, extra.compressionMode); + writeBytes(writer, extra.data); + } + + writer.view.setUint32( + lengthOffset - 4, + writer.offset - lengthOffset, + false + ); + } ); -addHandlerAlias('FXid', 'FEid'); +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); - }, + "FMsk", + hasKey("filterMask"), + async (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; + "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'); - }, + "artd", // document-wide artboard info + (target) => (target as Psd).artboards !== undefined, + async (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, await 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); + 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, _, options) => { - 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, !!options.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); - }, + "lfx2", + (target) => target.effects !== undefined && !hasMultiEffects(target.effects), + async (reader, target, left, _, options) => { + 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, !!options.logMissingFeatures); + + skipBytes(reader, await 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'; + 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.enableCompCore) target.compositorUsed.enableCompCore = enumValue(desc.enableCompCore); - if (desc.enableCompCoreGPU) target.compositorUsed.enableCompCoreGPU = enumValue(desc.enableCompCoreGPU); - 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: { major: 1, minor: 0, fix: 0 }, // TEMP - // psVersion: { major: 22, minor: 3, fix: 1 }, // TESTING - description: cinf.description, - reason: cinf.reason, - Engn: `Engn.${cinf.engine}`, - }; - - if (cinf.enableCompCore) desc.enableCompCore = `enable.${cinf.enableCompCore}`; - if (cinf.enableCompCoreGPU) desc.enableCompCoreGPU = `enable.${cinf.enableCompCoreGPU}`; - // desc.enableCompCoreThreads = `enable.feature`; // TESTING - if (cinf.compCoreSupport) desc.compCoreSupport = `reason.${cinf.compCoreSupport}`; - if (cinf.compCoreGPUSupport) desc.compCoreGPUSupport = `reason.${cinf.compCoreGPUSupport}`; - - writeVersionAndDescriptor(writer, '', 'null', desc); - }, + "cinf", + hasKey("compositorUsed"), + async (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.enableCompCore) + target.compositorUsed.enableCompCore = enumValue(desc.enableCompCore); + if (desc.enableCompCoreGPU) + target.compositorUsed.enableCompCoreGPU = enumValue( + desc.enableCompCoreGPU + ); + if (desc.compCoreSupport) + target.compositorUsed.compCoreSupport = enumValue(desc.compCoreSupport); + if (desc.compCoreGPUSupport) + target.compositorUsed.compCoreGPUSupport = enumValue( + desc.compCoreGPUSupport + ); + + skipBytes(reader, await left()); + }, + (writer, target) => { + const cinf = target.compositorUsed!; + const desc: CinfDescriptor = { + Vrsn: { major: 1, minor: 0, fix: 0 }, // TEMP + // psVersion: { major: 22, minor: 3, fix: 1 }, // TESTING + description: cinf.description, + reason: cinf.reason, + Engn: `Engn.${cinf.engine}`, + }; + + if (cinf.enableCompCore) + desc.enableCompCore = `enable.${cinf.enableCompCore}`; + if (cinf.enableCompCoreGPU) + desc.enableCompCoreGPU = `enable.${cinf.enableCompCoreGPU}`; + // desc.enableCompCoreThreads = `enable.feature`; // TESTING + 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; - }; + 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); - }, + "extn", + (target) => (target as any)._extn !== undefined, + async (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); - }, + "iOpa", + hasKey("fillOpacity"), + async (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); - } - }, + "brst", + hasKey("channelBlendingRestrictions"), + async (reader, target, left) => { + target.channelBlendingRestrictions = []; + + while ((await 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); - }, + "tsly", + hasKey("transparencyShapesLayer"), + async (reader, target) => { + target.transparencyShapesLayer = !!readUint8(reader); + skipBytes(reader, 3); + }, + (writer, target) => { + writeUint8(writer, target.transparencyShapesLayer ? 1 : 0); + writeZeros(writer, 3); + } ); diff --git a/src/imageResources.ts b/src/imageResources.ts index 84d08b7..423b472 100644 --- a/src/imageResources.ts +++ b/src/imageResources.ts @@ -1,1504 +1,1700 @@ -import { toByteArray } from 'base64-js'; -import { BlendMode, ImageResources, ReadOptions, RenderingIntent } from './psd'; +import { toByteArray } from "base64-js"; +import { BlendMode, ImageResources, ReadOptions, RenderingIntent } from "./psd"; import { - PsdReader, readPascalString, readUnicodeString, readUint32, readUint16, readUint8, readFloat64, - readBytes, skipBytes, readFloat32, readInt16, readFixedPoint32, readSignature, checkSignature, - readSection, readColor, readInt32 -} from './psdReader'; + PsdReader, + readPascalString, + readUnicodeString, + readUint32, + readUint16, + readUint8, + readFloat64, + readBytes, + skipBytes, + readFloat32, + readInt16, + readFixedPoint32, + readSignature, + checkSignature, + readSection, + readColor, + readInt32, +} from "./psdReader"; import { - PsdWriter, writePascalString, writeUnicodeString, writeUint32, writeUint8, writeFloat64, writeUint16, - writeBytes, writeInt16, writeFloat32, writeFixedPoint32, writeUnicodeStringWithPadding, writeColor, writeSignature, - writeSection, writeInt32, -} from './psdWriter'; -import { createCanvasFromData, createEnum, MOCK_HANDLERS } from './helpers'; -import { decodeString, encodeString } from './utf8'; + PsdWriter, + writePascalString, + writeUnicodeString, + writeUint32, + writeUint8, + writeFloat64, + writeUint16, + writeBytes, + writeInt16, + writeFloat32, + writeFixedPoint32, + writeUnicodeStringWithPadding, + writeColor, + writeSignature, + writeSection, + writeInt32, +} from "./psdWriter"; +import { createCanvasFromData, createEnum, MOCK_HANDLERS } from "./helpers"; +import { decodeString, encodeString } from "./utf8"; import { - ESliceBGColorType, ESliceHorzAlign, ESliceOrigin, ESliceType, ESliceVertAlign, frac, - FractionDescriptor, parseTrackList, readVersionAndDescriptor, serializeTrackList, TimelineTrackDescriptor, - TimeScopeDescriptor, writeVersionAndDescriptor -} from './descriptor'; + ESliceBGColorType, + ESliceHorzAlign, + ESliceOrigin, + ESliceType, + ESliceVertAlign, + frac, + FractionDescriptor, + parseTrackList, + readVersionAndDescriptor, + serializeTrackList, + TimelineTrackDescriptor, + TimeScopeDescriptor, + writeVersionAndDescriptor, +} from "./descriptor"; export interface ResourceHandler { - key: number; - has: (target: ImageResources) => boolean | number; - read: (reader: PsdReader, target: ImageResources, left: () => number, options: ReadOptions) => void; - write: (writer: PsdWriter, target: ImageResources, index: number) => void; + key: number; + has: (target: ImageResources) => boolean | number; + read: ( + reader: PsdReader, + target: ImageResources, + left: () => Promise, + options: ReadOptions + ) => Promise; + write: (writer: PsdWriter, target: ImageResources, index: number) => void; } export const resourceHandlers: ResourceHandler[] = []; export const resourceHandlersMap: { [key: number]: ResourceHandler } = {}; function addHandler( - key: number, - has: (target: ImageResources) => boolean | number, - read: (reader: PsdReader, target: ImageResources, left: () => number, options: ReadOptions) => void, - write: (writer: PsdWriter, target: ImageResources, index: number) => void, + key: number, + has: (target: ImageResources) => boolean | number, + read: ( + reader: PsdReader, + target: ImageResources, + left: () => Promise, + options: ReadOptions + ) => Promise, + write: (writer: PsdWriter, target: ImageResources, index: number) => void ) { - const handler: ResourceHandler = { key, has, read, write }; - resourceHandlers.push(handler); - resourceHandlersMap[handler.key] = handler; + const handler: ResourceHandler = { key, has, read, write }; + resourceHandlers.push(handler); + resourceHandlersMap[handler.key] = handler; } const LOG_MOCK_HANDLERS = false; -const RESOLUTION_UNITS = [undefined, 'PPI', 'PPCM']; -const MEASUREMENT_UNITS = [undefined, 'Inches', 'Centimeters', 'Points', 'Picas', 'Columns']; -const hex = '0123456789abcdef'; +const RESOLUTION_UNITS = [undefined, "PPI", "PPCM"]; +const MEASUREMENT_UNITS = [ + undefined, + "Inches", + "Centimeters", + "Points", + "Picas", + "Columns", +]; +const hex = "0123456789abcdef"; function charToNibble(code: number) { - return code <= 57 ? code - 48 : code - 87; + return code <= 57 ? code - 48 : code - 87; } function byteAt(value: string, index: number) { - return (charToNibble(value.charCodeAt(index)) << 4) | charToNibble(value.charCodeAt(index + 1)); + return ( + (charToNibble(value.charCodeAt(index)) << 4) | + charToNibble(value.charCodeAt(index + 1)) + ); } function readUtf8String(reader: PsdReader, length: number) { - const buffer = readBytes(reader, length); - return decodeString(buffer); + const buffer = readBytes(reader, length); + return decodeString(buffer); } function writeUtf8String(writer: PsdWriter, value: string) { - const buffer = encodeString(value); - writeBytes(writer, buffer); + const buffer = encodeString(value); + writeBytes(writer, buffer); } -MOCK_HANDLERS && addHandler( - 1028, // IPTC-NAA record - target => (target as any)._ir1028 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1028', left()); - (target as any)._ir1028 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1028); - }, -); +MOCK_HANDLERS && + addHandler( + 1028, // IPTC-NAA record + (target) => (target as any)._ir1028 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1028", await left()); + (target as any)._ir1028 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1028); + } + ); addHandler( - 1061, - target => target.captionDigest !== undefined, - (reader, target) => { - let captionDigest = ''; - - for (let i = 0; i < 16; i++) { - const byte = readUint8(reader); - captionDigest += hex[byte >> 4]; - captionDigest += hex[byte & 0xf]; - } - - target.captionDigest = captionDigest; - }, - (writer, target) => { - for (let i = 0; i < 16; i++) { - writeUint8(writer, byteAt(target.captionDigest!, i * 2)); - } - }, + 1061, + (target) => target.captionDigest !== undefined, + async (reader, target) => { + let captionDigest = ""; + + for (let i = 0; i < 16; i++) { + const byte = readUint8(reader); + captionDigest += hex[byte >> 4]; + captionDigest += hex[byte & 0xf]; + } + + target.captionDigest = captionDigest; + }, + (writer, target) => { + for (let i = 0; i < 16; i++) { + writeUint8(writer, byteAt(target.captionDigest!, i * 2)); + } + } ); addHandler( - 1060, - target => target.xmpMetadata !== undefined, - (reader, target, left) => { - target.xmpMetadata = readUtf8String(reader, left()); - }, - (writer, target) => { - writeUtf8String(writer, target.xmpMetadata!); - }, + 1060, + (target) => target.xmpMetadata !== undefined, + async (reader, target, left) => { + target.xmpMetadata = readUtf8String(reader, await left()); + }, + (writer, target) => { + writeUtf8String(writer, target.xmpMetadata!); + } ); -const Inte = createEnum('Inte', 'perceptual', { - 'perceptual': 'Img ', - 'saturation': 'Grp ', - 'relative colorimetric': 'Clrm', - 'absolute colorimetric': 'AClr', +const Inte = createEnum("Inte", "perceptual", { + perceptual: "Img ", + saturation: "Grp ", + "relative colorimetric": "Clrm", + "absolute colorimetric": "AClr", }); interface PrintInformationDescriptor { - 'Nm '?: string; - ClrS?: string; - PstS?: boolean; - MpBl?: boolean; - Inte?: string; - hardProof?: boolean; - printSixteenBit?: boolean; - printerName?: string; - printProofSetup?: { - Bltn: string; - } | { - profile: string; - Inte: string; - MpBl: boolean; - paperWhite: boolean; - }; + "Nm "?: string; + ClrS?: string; + PstS?: boolean; + MpBl?: boolean; + Inte?: string; + hardProof?: boolean; + printSixteenBit?: boolean; + printerName?: string; + printProofSetup?: + | { + Bltn: string; + } + | { + profile: string; + Inte: string; + MpBl: boolean; + paperWhite: boolean; + }; } addHandler( - 1082, - target => target.printInformation !== undefined, - (reader, target) => { - const desc: PrintInformationDescriptor = readVersionAndDescriptor(reader); - - target.printInformation = { - printerName: desc.printerName || '', - renderingIntent: Inte.decode(desc.Inte ?? 'Inte.Img '), - }; - - const info = target.printInformation; - - if (desc.PstS !== undefined) info.printerManagesColors = desc.PstS; - if (desc['Nm '] !== undefined) info.printerProfile = desc['Nm ']; - if (desc.MpBl !== undefined) info.blackPointCompensation = desc.MpBl; - if (desc.printSixteenBit !== undefined) info.printSixteenBit = desc.printSixteenBit; - if (desc.hardProof !== undefined) info.hardProof = desc.hardProof; - if (desc.printProofSetup) { - if ('Bltn' in desc.printProofSetup) { - info.proofSetup = { builtin: desc.printProofSetup.Bltn.split('.')[1] }; - } else { - info.proofSetup = { - profile: desc.printProofSetup.profile, - renderingIntent: Inte.decode(desc.printProofSetup.Inte ?? 'Inte.Img '), - blackPointCompensation: !!desc.printProofSetup.MpBl, - paperWhite: !!desc.printProofSetup.paperWhite, - }; - } - } - }, - (writer, target) => { - const info = target.printInformation!; - const desc: PrintInformationDescriptor = {}; - - if (info.printerManagesColors) { - desc.PstS = true; - } else { - if (info.hardProof !== undefined) desc.hardProof = !!info.hardProof; - desc.ClrS = 'ClrS.RGBC'; // TODO: ??? - desc['Nm '] = info.printerProfile ?? 'CIE RGB'; - } - - desc.Inte = Inte.encode(info.renderingIntent); - - if (!info.printerManagesColors) desc.MpBl = !!info.blackPointCompensation; - - desc.printSixteenBit = !!info.printSixteenBit; - desc.printerName = info.printerName || ''; - - if (info.proofSetup && 'profile' in info.proofSetup) { - desc.printProofSetup = { - profile: info.proofSetup.profile || '', - Inte: Inte.encode(info.proofSetup.renderingIntent), - MpBl: !!info.proofSetup.blackPointCompensation, - paperWhite: !!info.proofSetup.paperWhite, - }; - } else { - desc.printProofSetup = { - Bltn: info.proofSetup?.builtin ? `builtinProof.${info.proofSetup.builtin}` : 'builtinProof.proofCMYK', - }; - } - - writeVersionAndDescriptor(writer, '', 'printOutput', desc); - }, + 1082, + (target) => target.printInformation !== undefined, + async (reader, target) => { + const desc: PrintInformationDescriptor = readVersionAndDescriptor(reader); + + target.printInformation = { + printerName: desc.printerName || "", + renderingIntent: Inte.decode(desc.Inte ?? "Inte.Img "), + }; + + const info = target.printInformation; + + if (desc.PstS !== undefined) info.printerManagesColors = desc.PstS; + if (desc["Nm "] !== undefined) info.printerProfile = desc["Nm "]; + if (desc.MpBl !== undefined) info.blackPointCompensation = desc.MpBl; + if (desc.printSixteenBit !== undefined) + info.printSixteenBit = desc.printSixteenBit; + if (desc.hardProof !== undefined) info.hardProof = desc.hardProof; + if (desc.printProofSetup) { + if ("Bltn" in desc.printProofSetup) { + info.proofSetup = { builtin: desc.printProofSetup.Bltn.split(".")[1] }; + } else { + info.proofSetup = { + profile: desc.printProofSetup.profile, + renderingIntent: Inte.decode( + desc.printProofSetup.Inte ?? "Inte.Img " + ), + blackPointCompensation: !!desc.printProofSetup.MpBl, + paperWhite: !!desc.printProofSetup.paperWhite, + }; + } + } + }, + (writer, target) => { + const info = target.printInformation!; + const desc: PrintInformationDescriptor = {}; + + if (info.printerManagesColors) { + desc.PstS = true; + } else { + if (info.hardProof !== undefined) desc.hardProof = !!info.hardProof; + desc.ClrS = "ClrS.RGBC"; // TODO: ??? + desc["Nm "] = info.printerProfile ?? "CIE RGB"; + } + + desc.Inte = Inte.encode(info.renderingIntent); + + if (!info.printerManagesColors) desc.MpBl = !!info.blackPointCompensation; + + desc.printSixteenBit = !!info.printSixteenBit; + desc.printerName = info.printerName || ""; + + if (info.proofSetup && "profile" in info.proofSetup) { + desc.printProofSetup = { + profile: info.proofSetup.profile || "", + Inte: Inte.encode(info.proofSetup.renderingIntent), + MpBl: !!info.proofSetup.blackPointCompensation, + paperWhite: !!info.proofSetup.paperWhite, + }; + } else { + desc.printProofSetup = { + Bltn: info.proofSetup?.builtin + ? `builtinProof.${info.proofSetup.builtin}` + : "builtinProof.proofCMYK", + }; + } + + writeVersionAndDescriptor(writer, "", "printOutput", desc); + } ); -MOCK_HANDLERS && addHandler( - 1083, // Print style - target => (target as any)._ir1083 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1083', left()); - (target as any)._ir1083 = readBytes(reader, left()); - - // TODO: - // const desc = readVersionAndDescriptor(reader); - // console.log('1083', require('util').inspect(desc, false, 99, true)); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1083); - }, -); +MOCK_HANDLERS && + addHandler( + 1083, // Print style + (target) => (target as any)._ir1083 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1083", await left()); + (target as any)._ir1083 = readBytes(reader, await left()); + + // TODO: + // const desc = readVersionAndDescriptor(reader); + // console.log('1083', require('util').inspect(desc, false, 99, true)); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1083); + } + ); addHandler( - 1005, - target => target.resolutionInfo !== undefined, - (reader, target) => { - const horizontalResolution = readFixedPoint32(reader); - const horizontalResolutionUnit = readUint16(reader); - const widthUnit = readUint16(reader); - const verticalResolution = readFixedPoint32(reader); - const verticalResolutionUnit = readUint16(reader); - const heightUnit = readUint16(reader); - - target.resolutionInfo = { - horizontalResolution, - horizontalResolutionUnit: RESOLUTION_UNITS[horizontalResolutionUnit] || 'PPI' as any, - widthUnit: MEASUREMENT_UNITS[widthUnit] || 'Inches' as any, - verticalResolution, - verticalResolutionUnit: RESOLUTION_UNITS[verticalResolutionUnit] || 'PPI' as any, - heightUnit: MEASUREMENT_UNITS[heightUnit] || 'Inches' as any, - }; - }, - (writer, target) => { - const info = target.resolutionInfo!; - - writeFixedPoint32(writer, info.horizontalResolution || 0); - writeUint16(writer, Math.max(1, RESOLUTION_UNITS.indexOf(info.horizontalResolutionUnit))); - writeUint16(writer, Math.max(1, MEASUREMENT_UNITS.indexOf(info.widthUnit))); - writeFixedPoint32(writer, info.verticalResolution || 0); - writeUint16(writer, Math.max(1, RESOLUTION_UNITS.indexOf(info.verticalResolutionUnit))); - writeUint16(writer, Math.max(1, MEASUREMENT_UNITS.indexOf(info.heightUnit))); - }, + 1005, + (target) => target.resolutionInfo !== undefined, + async (reader, target) => { + const horizontalResolution = readFixedPoint32(reader); + const horizontalResolutionUnit = readUint16(reader); + const widthUnit = readUint16(reader); + const verticalResolution = readFixedPoint32(reader); + const verticalResolutionUnit = readUint16(reader); + const heightUnit = readUint16(reader); + + target.resolutionInfo = { + horizontalResolution, + horizontalResolutionUnit: + RESOLUTION_UNITS[horizontalResolutionUnit] || ("PPI" as any), + widthUnit: MEASUREMENT_UNITS[widthUnit] || ("Inches" as any), + verticalResolution, + verticalResolutionUnit: + RESOLUTION_UNITS[verticalResolutionUnit] || ("PPI" as any), + heightUnit: MEASUREMENT_UNITS[heightUnit] || ("Inches" as any), + }; + }, + (writer, target) => { + const info = target.resolutionInfo!; + + writeFixedPoint32(writer, info.horizontalResolution || 0); + writeUint16( + writer, + Math.max(1, RESOLUTION_UNITS.indexOf(info.horizontalResolutionUnit)) + ); + writeUint16(writer, Math.max(1, MEASUREMENT_UNITS.indexOf(info.widthUnit))); + writeFixedPoint32(writer, info.verticalResolution || 0); + writeUint16( + writer, + Math.max(1, RESOLUTION_UNITS.indexOf(info.verticalResolutionUnit)) + ); + writeUint16( + writer, + Math.max(1, MEASUREMENT_UNITS.indexOf(info.heightUnit)) + ); + } ); -const printScaleStyles = ['centered', 'size to fit', 'user defined']; +const printScaleStyles = ["centered", "size to fit", "user defined"]; addHandler( - 1062, - target => target.printScale !== undefined, - (reader, target) => { - target.printScale = { - style: printScaleStyles[readInt16(reader)] as any, - x: readFloat32(reader), - y: readFloat32(reader), - scale: readFloat32(reader), - }; - }, - (writer, target) => { - const { style, x, y, scale } = target.printScale!; - writeInt16(writer, Math.max(0, printScaleStyles.indexOf(style!))); - writeFloat32(writer, x || 0); - writeFloat32(writer, y || 0); - writeFloat32(writer, scale || 0); - }, + 1062, + (target) => target.printScale !== undefined, + async (reader, target) => { + target.printScale = { + style: printScaleStyles[readInt16(reader)] as any, + x: readFloat32(reader), + y: readFloat32(reader), + scale: readFloat32(reader), + }; + }, + (writer, target) => { + const { style, x, y, scale } = target.printScale!; + writeInt16(writer, Math.max(0, printScaleStyles.indexOf(style!))); + writeFloat32(writer, x || 0); + writeFloat32(writer, y || 0); + writeFloat32(writer, scale || 0); + } ); addHandler( - 1006, - target => target.alphaChannelNames !== undefined, - (reader, target, left) => { - target.alphaChannelNames = []; - - while (left() > 0) { - const value = readPascalString(reader, 1); - target.alphaChannelNames.push(value); - } - }, - (writer, target) => { - for (const name of target.alphaChannelNames!) { - writePascalString(writer, name, 1); - } - }, + 1006, + (target) => target.alphaChannelNames !== undefined, + async (reader, target, left) => { + target.alphaChannelNames = []; + + while ((await left()) > 0) { + const value = readPascalString(reader, 1); + target.alphaChannelNames.push(value); + } + }, + (writer, target) => { + for (const name of target.alphaChannelNames!) { + writePascalString(writer, name, 1); + } + } ); addHandler( - 1045, - target => target.alphaChannelNames !== undefined, - (reader, target, left) => { - target.alphaChannelNames = []; - - while (left() > 0) { - target.alphaChannelNames.push(readUnicodeString(reader)); - } - }, - (writer, target) => { - for (const name of target.alphaChannelNames!) { - writeUnicodeStringWithPadding(writer, name); - } - }, + 1045, + (target) => target.alphaChannelNames !== undefined, + async (reader, target, left) => { + target.alphaChannelNames = []; + + while ((await left()) > 0) { + target.alphaChannelNames.push(readUnicodeString(reader)); + } + }, + (writer, target) => { + for (const name of target.alphaChannelNames!) { + writeUnicodeStringWithPadding(writer, name); + } + } ); -MOCK_HANDLERS && addHandler( - 1077, - target => (target as any)._ir1077 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1077', left()); - (target as any)._ir1077 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1077); - }, -); +MOCK_HANDLERS && + addHandler( + 1077, + (target) => (target as any)._ir1077 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1077", await left()); + (target as any)._ir1077 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1077); + } + ); addHandler( - 1053, - target => target.alphaIdentifiers !== undefined, - (reader, target, left) => { - target.alphaIdentifiers = []; - - while (left() >= 4) { - target.alphaIdentifiers.push(readUint32(reader)); - } - }, - (writer, target) => { - for (const id of target.alphaIdentifiers!) { - writeUint32(writer, id); - } - }, + 1053, + (target) => target.alphaIdentifiers !== undefined, + async (reader, target, left) => { + target.alphaIdentifiers = []; + + while ((await left()) >= 4) { + target.alphaIdentifiers.push(readUint32(reader)); + } + }, + (writer, target) => { + for (const id of target.alphaIdentifiers!) { + writeUint32(writer, id); + } + } ); addHandler( - 1010, - target => target.backgroundColor !== undefined, - (reader, target) => target.backgroundColor = readColor(reader), - (writer, target) => writeColor(writer, target.backgroundColor!), + 1010, + (target) => target.backgroundColor !== undefined, + async (reader, target) => { + target.backgroundColor = readColor(reader); + }, + (writer, target) => writeColor(writer, target.backgroundColor!) ); addHandler( - 1037, - target => target.globalAngle !== undefined, - (reader, target) => target.globalAngle = readInt32(reader), - (writer, target) => writeInt32(writer, target.globalAngle!), + 1037, + (target) => target.globalAngle !== undefined, + async (reader, target) => { + target.globalAngle = readInt32(reader); + }, + (writer, target) => writeInt32(writer, target.globalAngle!) ); addHandler( - 1049, - target => target.globalAltitude !== undefined, - (reader, target) => target.globalAltitude = readUint32(reader), - (writer, target) => writeUint32(writer, target.globalAltitude!), + 1049, + (target) => target.globalAltitude !== undefined, + async (reader, target) => { + target.globalAltitude = readUint32(reader); + }, + (writer, target) => writeUint32(writer, target.globalAltitude!) ); addHandler( - 1011, - target => target.printFlags !== undefined, - (reader, target) => { - target.printFlags = { - labels: !!readUint8(reader), - cropMarks: !!readUint8(reader), - colorBars: !!readUint8(reader), - registrationMarks: !!readUint8(reader), - negative: !!readUint8(reader), - flip: !!readUint8(reader), - interpolate: !!readUint8(reader), - caption: !!readUint8(reader), - printFlags: !!readUint8(reader), - }; - }, - (writer, target) => { - const flags = target.printFlags!; - writeUint8(writer, flags.labels ? 1 : 0); - writeUint8(writer, flags.cropMarks ? 1 : 0); - writeUint8(writer, flags.colorBars ? 1 : 0); - writeUint8(writer, flags.registrationMarks ? 1 : 0); - writeUint8(writer, flags.negative ? 1 : 0); - writeUint8(writer, flags.flip ? 1 : 0); - writeUint8(writer, flags.interpolate ? 1 : 0); - writeUint8(writer, flags.caption ? 1 : 0); - writeUint8(writer, flags.printFlags ? 1 : 0); - }, + 1011, + (target) => target.printFlags !== undefined, + async (reader, target) => { + target.printFlags = { + labels: !!readUint8(reader), + cropMarks: !!readUint8(reader), + colorBars: !!readUint8(reader), + registrationMarks: !!readUint8(reader), + negative: !!readUint8(reader), + flip: !!readUint8(reader), + interpolate: !!readUint8(reader), + caption: !!readUint8(reader), + printFlags: !!readUint8(reader), + }; + }, + (writer, target) => { + const flags = target.printFlags!; + writeUint8(writer, flags.labels ? 1 : 0); + writeUint8(writer, flags.cropMarks ? 1 : 0); + writeUint8(writer, flags.colorBars ? 1 : 0); + writeUint8(writer, flags.registrationMarks ? 1 : 0); + writeUint8(writer, flags.negative ? 1 : 0); + writeUint8(writer, flags.flip ? 1 : 0); + writeUint8(writer, flags.interpolate ? 1 : 0); + writeUint8(writer, flags.caption ? 1 : 0); + writeUint8(writer, flags.printFlags ? 1 : 0); + } ); -MOCK_HANDLERS && addHandler( - 10000, // Print flags - target => (target as any)._ir10000 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 10000', left()); - (target as any)._ir10000 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir10000); - }, -); - -MOCK_HANDLERS && addHandler( - 1013, // Color halftoning - target => (target as any)._ir1013 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1013', left()); - (target as any)._ir1013 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1013); - }, -); - -MOCK_HANDLERS && addHandler( - 1016, // Color transfer functions - target => (target as any)._ir1016 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1016', left()); - (target as any)._ir1016 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1016); - }, -); +MOCK_HANDLERS && + addHandler( + 10000, // Print flags + (target) => (target as any)._ir10000 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 10000", await left()); + (target as any)._ir10000 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir10000); + } + ); + +MOCK_HANDLERS && + addHandler( + 1013, // Color halftoning + (target) => (target as any)._ir1013 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1013", await left()); + (target as any)._ir1013 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1013); + } + ); + +MOCK_HANDLERS && + addHandler( + 1016, // Color transfer functions + (target) => (target as any)._ir1016 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1016", await left()); + (target as any)._ir1016 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1016); + } + ); interface CountInformationDesc { - Vrsn: 1; - countGroupList: { - 'Rd ': number; // 0-255 - 'Grn ': number; - 'Bl ': number; - 'Nm ': string; - 'Rds ': number; // Marker size - fontSize: number; - Vsbl: boolean; - countObjectList: { - 'X ': number; - 'Y ': number; - }[]; - }[]; + Vrsn: 1; + countGroupList: { + "Rd ": number; // 0-255 + "Grn ": number; + "Bl ": number; + "Nm ": string; + "Rds ": number; // Marker size + fontSize: number; + Vsbl: boolean; + countObjectList: { + "X ": number; + "Y ": number; + }[]; + }[]; } addHandler( - 1080, // Count Information - target => target.countInformation !== undefined, - (reader, target) => { - const desc = readVersionAndDescriptor(reader) as CountInformationDesc; - target.countInformation = desc.countGroupList.map(g => ({ - color: { r: g['Rd '], g: g['Grn '], b: g['Bl '] }, - name: g['Nm '], - size: g['Rds '], - fontSize: g.fontSize, - visible: g.Vsbl, - points: g.countObjectList.map(p => ({ x: p['X '], y: p['Y '] })), - })); - }, - (writer, target) => { - const desc: CountInformationDesc = { - Vrsn: 1, - countGroupList: target.countInformation!.map(g => ({ - 'Rd ': g.color.r, - 'Grn ': g.color.g, - 'Bl ': g.color.b, - 'Nm ': g.name, - 'Rds ': g.size, - fontSize: g.fontSize, - Vsbl: g.visible, - countObjectList: g.points.map(p => ({ 'X ': p.x, 'Y ': p.y })), - })), - }; - writeVersionAndDescriptor(writer, '', 'Cnt ', desc); - }, + 1080, // Count Information + (target) => target.countInformation !== undefined, + async (reader, target) => { + const desc = readVersionAndDescriptor(reader) as CountInformationDesc; + target.countInformation = desc.countGroupList.map((g) => ({ + color: { r: g["Rd "], g: g["Grn "], b: g["Bl "] }, + name: g["Nm "], + size: g["Rds "], + fontSize: g.fontSize, + visible: g.Vsbl, + points: g.countObjectList.map((p) => ({ x: p["X "], y: p["Y "] })), + })); + }, + (writer, target) => { + const desc: CountInformationDesc = { + Vrsn: 1, + countGroupList: target.countInformation!.map((g) => ({ + "Rd ": g.color.r, + "Grn ": g.color.g, + "Bl ": g.color.b, + "Nm ": g.name, + "Rds ": g.size, + fontSize: g.fontSize, + Vsbl: g.visible, + countObjectList: g.points.map((p) => ({ "X ": p.x, "Y ": p.y })), + })), + }; + writeVersionAndDescriptor(writer, "", "Cnt ", desc); + } ); addHandler( - 1024, - target => target.layerState !== undefined, - (reader, target) => target.layerState = readUint16(reader), - (writer, target) => writeUint16(writer, target.layerState!), + 1024, + (target) => target.layerState !== undefined, + async (reader, target) => { + target.layerState = readUint16(reader); + }, + (writer, target) => writeUint16(writer, target.layerState!) ); addHandler( - 1026, - target => target.layersGroup !== undefined, - (reader, target, left) => { - target.layersGroup = []; - - while (left() > 0) { - target.layersGroup.push(readUint16(reader)); - } - }, - (writer, target) => { - for (const g of target.layersGroup!) { - writeUint16(writer, g); - } - }, + 1026, + (target) => target.layersGroup !== undefined, + async (reader, target, left) => { + target.layersGroup = []; + + while ((await left()) > 0) { + target.layersGroup.push(readUint16(reader)); + } + }, + (writer, target) => { + for (const g of target.layersGroup!) { + writeUint16(writer, g); + } + } ); addHandler( - 1072, - target => target.layerGroupsEnabledId !== undefined, - (reader, target, left) => { - target.layerGroupsEnabledId = []; - - while (left() > 0) { - target.layerGroupsEnabledId.push(readUint8(reader)); - } - }, - (writer, target) => { - for (const id of target.layerGroupsEnabledId!) { - writeUint8(writer, id); - } - }, + 1072, + (target) => target.layerGroupsEnabledId !== undefined, + async (reader, target, left) => { + target.layerGroupsEnabledId = []; + + while ((await left()) > 0) { + target.layerGroupsEnabledId.push(readUint8(reader)); + } + }, + (writer, target) => { + for (const id of target.layerGroupsEnabledId!) { + writeUint8(writer, id); + } + } ); addHandler( - 1069, - target => target.layerSelectionIds !== undefined, - (reader, target) => { - let count = readUint16(reader); - target.layerSelectionIds = []; - - while (count--) { - target.layerSelectionIds.push(readUint32(reader)); - } - }, - (writer, target) => { - writeUint16(writer, target.layerSelectionIds!.length); - - for (const id of target.layerSelectionIds!) { - writeUint32(writer, id); - } - }, + 1069, + (target) => target.layerSelectionIds !== undefined, + async (reader, target) => { + let count = readUint16(reader); + target.layerSelectionIds = []; + + while (count--) { + target.layerSelectionIds.push(readUint32(reader)); + } + }, + (writer, target) => { + writeUint16(writer, target.layerSelectionIds!.length); + + for (const id of target.layerSelectionIds!) { + writeUint32(writer, id); + } + } ); addHandler( - 1032, - target => target.gridAndGuidesInformation !== undefined, - (reader, target) => { - const version = readUint32(reader); - const horizontal = readUint32(reader); - const vertical = readUint32(reader); - const count = readUint32(reader); - - if (version !== 1) throw new Error(`Invalid 1032 resource version: ${version}`); - - target.gridAndGuidesInformation = { - grid: { horizontal, vertical }, - guides: [], - }; - - for (let i = 0; i < count; i++) { - target.gridAndGuidesInformation.guides!.push({ - location: readUint32(reader) / 32, - direction: readUint8(reader) ? 'horizontal' : 'vertical' - }); - } - }, - (writer, target) => { - const info = target.gridAndGuidesInformation!; - const grid = info.grid || { horizontal: 18 * 32, vertical: 18 * 32 }; - const guides = info.guides || []; - - writeUint32(writer, 1); - writeUint32(writer, grid.horizontal); - writeUint32(writer, grid.vertical); - writeUint32(writer, guides.length); - - for (const g of guides) { - writeUint32(writer, g.location * 32); - writeUint8(writer, g.direction === 'horizontal' ? 1 : 0); - } - }, + 1032, + (target) => target.gridAndGuidesInformation !== undefined, + async (reader, target) => { + const version = readUint32(reader); + const horizontal = readUint32(reader); + const vertical = readUint32(reader); + const count = readUint32(reader); + + if (version !== 1) + throw new Error(`Invalid 1032 resource version: ${version}`); + + target.gridAndGuidesInformation = { + grid: { horizontal, vertical }, + guides: [], + }; + + for (let i = 0; i < count; i++) { + target.gridAndGuidesInformation.guides!.push({ + location: readUint32(reader) / 32, + direction: readUint8(reader) ? "horizontal" : "vertical", + }); + } + }, + (writer, target) => { + const info = target.gridAndGuidesInformation!; + const grid = info.grid || { horizontal: 18 * 32, vertical: 18 * 32 }; + const guides = info.guides || []; + + writeUint32(writer, 1); + writeUint32(writer, grid.horizontal); + writeUint32(writer, grid.vertical); + writeUint32(writer, guides.length); + + for (const g of guides) { + writeUint32(writer, g.location * 32); + writeUint8(writer, g.direction === "horizontal" ? 1 : 0); + } + } ); interface LayerCompsDescriptor { - list: { - _classID: 'Comp'; - 'Nm ': string; - compID: number; - capturedInfo: number; - comment?: string; - }[]; - lastAppliedComp?: number; + list: { + _classID: "Comp"; + "Nm ": string; + compID: number; + capturedInfo: number; + comment?: string; + }[]; + lastAppliedComp?: number; } addHandler( - 1065, // Layer Comps - target => target.layerComps !== undefined, - (reader, target) => { - const desc = readVersionAndDescriptor(reader, true) as LayerCompsDescriptor; - // console.log('CompList', require('util').inspect(desc, false, 99, true)); - - target.layerComps = { list: [] }; - - for (const item of desc.list) { - target.layerComps.list.push({ - id: item.compID, - name: item['Nm '], - capturedInfo: item.capturedInfo, - }); - - if ('comment' in item) target.layerComps.list[target.layerComps.list.length - 1].comment = item.comment; - } - - if ('lastAppliedComp' in desc) target.layerComps.lastApplied = desc.lastAppliedComp; - }, - (writer, target) => { - const layerComps = target.layerComps!; - const desc: LayerCompsDescriptor = { list: [] }; - - for (const item of layerComps.list) { - const t: LayerCompsDescriptor['list'][0] = {} as any; - t._classID = 'Comp'; - t['Nm '] = item.name; - if ('comment' in item) t.comment = item.comment; - t.compID = item.id; - t.capturedInfo = item.capturedInfo; - desc.list.push(t); - } - - if ('lastApplied' in layerComps) desc.lastAppliedComp = layerComps.lastApplied; - - // console.log('CompList', require('util').inspect(desc, false, 99, true)); - writeVersionAndDescriptor(writer, '', 'CompList', desc); - }, + 1065, // Layer Comps + (target) => target.layerComps !== undefined, + async (reader, target) => { + const desc = readVersionAndDescriptor(reader, true) as LayerCompsDescriptor; + // console.log('CompList', require('util').inspect(desc, false, 99, true)); + + target.layerComps = { list: [] }; + + for (const item of desc.list) { + target.layerComps.list.push({ + id: item.compID, + name: item["Nm "], + capturedInfo: item.capturedInfo, + }); + + if ("comment" in item) + target.layerComps.list[target.layerComps.list.length - 1].comment = + item.comment; + } + + if ("lastAppliedComp" in desc) + target.layerComps.lastApplied = desc.lastAppliedComp; + }, + (writer, target) => { + const layerComps = target.layerComps!; + const desc: LayerCompsDescriptor = { list: [] }; + + for (const item of layerComps.list) { + const t: LayerCompsDescriptor["list"][0] = {} as any; + t._classID = "Comp"; + t["Nm "] = item.name; + if ("comment" in item) t.comment = item.comment; + t.compID = item.id; + t.capturedInfo = item.capturedInfo; + desc.list.push(t); + } + + if ("lastApplied" in layerComps) + desc.lastAppliedComp = layerComps.lastApplied; + + // console.log('CompList', require('util').inspect(desc, false, 99, true)); + writeVersionAndDescriptor(writer, "", "CompList", desc); + } ); -MOCK_HANDLERS && addHandler( - 1092, // ??? - target => (target as any)._ir1092 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1092', left()); - // 16 bytes, seems to be 4 integers - (target as any)._ir1092 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1092); - }, -); +MOCK_HANDLERS && + addHandler( + 1092, // ??? + (target) => (target as any)._ir1092 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1092", await left()); + // 16 bytes, seems to be 4 integers + (target as any)._ir1092 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1092); + } + ); interface OnionSkinsDescriptor { - Vrsn: 1; - enab: boolean; - numBefore: number; - numAfter: number; - Spcn: number; - minOpacity: number; - maxOpacity: number; - BlnM: number; + Vrsn: 1; + enab: boolean; + numBefore: number; + numAfter: number; + Spcn: number; + minOpacity: number; + maxOpacity: number; + BlnM: number; } // 0 - normal, 7 - multiply, 8 - screen, 23 - difference const onionSkinsBlendModes: (BlendMode | undefined)[] = [ - 'normal', undefined, undefined, undefined, undefined, undefined, undefined, 'multiply', - 'screen', undefined, undefined, undefined, undefined, undefined, undefined, undefined, - undefined, undefined, undefined, undefined, undefined, undefined, undefined, 'difference', + "normal", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "multiply", + "screen", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "difference", ]; addHandler( - 1078, // Onion Skins - target => target.onionSkins !== undefined, - (reader, target) => { - const desc = readVersionAndDescriptor(reader) as OnionSkinsDescriptor; - // console.log('1078', require('util').inspect(desc, false, 99, true)); - - target.onionSkins = { - enabled: desc.enab, - framesBefore: desc.numBefore, - framesAfter: desc.numAfter, - frameSpacing: desc.Spcn, - minOpacity: desc.minOpacity / 100, - maxOpacity: desc.maxOpacity / 100, - blendMode: onionSkinsBlendModes[desc.BlnM] || 'normal', - }; - }, - (writer, target) => { - const onionSkins = target.onionSkins!; - const desc: OnionSkinsDescriptor = { - Vrsn: 1, - enab: onionSkins.enabled, - numBefore: onionSkins.framesBefore, - numAfter: onionSkins.framesAfter, - Spcn: onionSkins.frameSpacing, - minOpacity: (onionSkins.minOpacity * 100) | 0, - maxOpacity: (onionSkins.maxOpacity * 100) | 0, - BlnM: Math.max(0, onionSkinsBlendModes.indexOf(onionSkins.blendMode)), - }; - - writeVersionAndDescriptor(writer, '', 'null', desc); - }, + 1078, // Onion Skins + (target) => target.onionSkins !== undefined, + async (reader, target) => { + const desc = readVersionAndDescriptor(reader) as OnionSkinsDescriptor; + // console.log('1078', require('util').inspect(desc, false, 99, true)); + + target.onionSkins = { + enabled: desc.enab, + framesBefore: desc.numBefore, + framesAfter: desc.numAfter, + frameSpacing: desc.Spcn, + minOpacity: desc.minOpacity / 100, + maxOpacity: desc.maxOpacity / 100, + blendMode: onionSkinsBlendModes[desc.BlnM] || "normal", + }; + }, + (writer, target) => { + const onionSkins = target.onionSkins!; + const desc: OnionSkinsDescriptor = { + Vrsn: 1, + enab: onionSkins.enabled, + numBefore: onionSkins.framesBefore, + numAfter: onionSkins.framesAfter, + Spcn: onionSkins.frameSpacing, + minOpacity: (onionSkins.minOpacity * 100) | 0, + maxOpacity: (onionSkins.maxOpacity * 100) | 0, + BlnM: Math.max(0, onionSkinsBlendModes.indexOf(onionSkins.blendMode)), + }; + + writeVersionAndDescriptor(writer, "", "null", desc); + } ); interface TimelineAudioClipDescriptor { - clipID: string; - timeScope: TimeScopeDescriptor; - frameReader: { - frameReaderType: number; - descVersion: 1; - 'Lnk ': { - descVersion: 1; - 'Nm ': string; - fullPath: string; - relPath: string; - }, - mediaDescriptor: string; - }, - muted: boolean; - audioLevel: number; + clipID: string; + timeScope: TimeScopeDescriptor; + frameReader: { + frameReaderType: number; + descVersion: 1; + "Lnk ": { + descVersion: 1; + "Nm ": string; + fullPath: string; + relPath: string; + }; + mediaDescriptor: string; + }; + muted: boolean; + audioLevel: number; } interface TimelineAudioClipGroupDescriptor { - groupID: string; - muted: boolean; - audioClipList: TimelineAudioClipDescriptor[]; + groupID: string; + muted: boolean; + audioClipList: TimelineAudioClipDescriptor[]; } interface TimelineInformationDescriptor { - Vrsn: 1; - enab: boolean; - frameStep: FractionDescriptor; - frameRate: number; - time: FractionDescriptor; - duration: FractionDescriptor; - workInTime: FractionDescriptor; - workOutTime: FractionDescriptor; - LCnt: number; - globalTrackList: TimelineTrackDescriptor[]; - audioClipGroupList?: { - audioClipGroupList?: TimelineAudioClipGroupDescriptor[]; - }, - hasMotion: boolean; + Vrsn: 1; + enab: boolean; + frameStep: FractionDescriptor; + frameRate: number; + time: FractionDescriptor; + duration: FractionDescriptor; + workInTime: FractionDescriptor; + workOutTime: FractionDescriptor; + LCnt: number; + globalTrackList: TimelineTrackDescriptor[]; + audioClipGroupList?: { + audioClipGroupList?: TimelineAudioClipGroupDescriptor[]; + }; + hasMotion: boolean; } addHandler( - 1075, // Timeline Information - target => target.timelineInformation !== undefined, - (reader, target, _, options) => { - const desc = readVersionAndDescriptor(reader) as TimelineInformationDescriptor; - - target.timelineInformation = { - enabled: desc.enab, - frameStep: frac(desc.frameStep), - frameRate: desc.frameRate, - time: frac(desc.time), - duration: frac(desc.duration), - workInTime: frac(desc.workInTime), - workOutTime: frac(desc.workOutTime), - repeats: desc.LCnt, - hasMotion: desc.hasMotion, - globalTracks: parseTrackList(desc.globalTrackList, !!options.logMissingFeatures), - }; - - if (desc.audioClipGroupList?.audioClipGroupList?.length) { - target.timelineInformation.audioClipGroups = desc.audioClipGroupList.audioClipGroupList.map(g => ({ - id: g.groupID, - muted: g.muted, - audioClips: g.audioClipList.map(({ clipID, timeScope, muted, audioLevel, frameReader }) => ({ - id: clipID, - start: frac(timeScope.Strt), - duration: frac(timeScope.duration), - inTime: frac(timeScope.inTime), - outTime: frac(timeScope.outTime), - muted: muted, - audioLevel: audioLevel, - frameReader: { - type: frameReader.frameReaderType, - mediaDescriptor: frameReader.mediaDescriptor, - link: { - name: frameReader['Lnk ']['Nm '], - fullPath: frameReader['Lnk '].fullPath, - relativePath: frameReader['Lnk '].relPath, - }, - }, - })), - })); - } - }, - (writer, target) => { - const timeline = target.timelineInformation!; - const desc: TimelineInformationDescriptor = { - Vrsn: 1, - enab: timeline.enabled, - frameStep: timeline.frameStep, - frameRate: timeline.frameRate, - time: timeline.time, - duration: timeline.duration, - workInTime: timeline.workInTime, - workOutTime: timeline.workOutTime, - LCnt: timeline.repeats, - globalTrackList: serializeTrackList(timeline.globalTracks), - audioClipGroupList: { - audioClipGroupList: timeline.audioClipGroups?.map(a => ({ - groupID: a.id, - muted: a.muted, - audioClipList: a.audioClips.map(c => ({ - clipID: c.id, - timeScope: { - Vrsn: 1, - Strt: c.start, - duration: c.duration, - inTime: c.inTime, - outTime: c.outTime, - }, - frameReader: { - frameReaderType: c.frameReader.type, - descVersion: 1, - 'Lnk ': { - descVersion: 1, - 'Nm ': c.frameReader.link.name, - fullPath: c.frameReader.link.fullPath, - relPath: c.frameReader.link.relativePath, - }, - mediaDescriptor: c.frameReader.mediaDescriptor, - }, - muted: c.muted, - audioLevel: c.audioLevel, - })), - })), - }, - hasMotion: timeline.hasMotion, - }; - - writeVersionAndDescriptor(writer, '', 'null', desc, 'anim'); - }, + 1075, // Timeline Information + (target) => target.timelineInformation !== undefined, + async (reader, target, _, options) => { + const desc = readVersionAndDescriptor( + reader + ) as TimelineInformationDescriptor; + + target.timelineInformation = { + enabled: desc.enab, + frameStep: frac(desc.frameStep), + frameRate: desc.frameRate, + time: frac(desc.time), + duration: frac(desc.duration), + workInTime: frac(desc.workInTime), + workOutTime: frac(desc.workOutTime), + repeats: desc.LCnt, + hasMotion: desc.hasMotion, + globalTracks: parseTrackList( + desc.globalTrackList, + !!options.logMissingFeatures + ), + }; + + if (desc.audioClipGroupList?.audioClipGroupList?.length) { + target.timelineInformation.audioClipGroups = + desc.audioClipGroupList.audioClipGroupList.map((g) => ({ + id: g.groupID, + muted: g.muted, + audioClips: g.audioClipList.map( + ({ clipID, timeScope, muted, audioLevel, frameReader }) => ({ + id: clipID, + start: frac(timeScope.Strt), + duration: frac(timeScope.duration), + inTime: frac(timeScope.inTime), + outTime: frac(timeScope.outTime), + muted: muted, + audioLevel: audioLevel, + frameReader: { + type: frameReader.frameReaderType, + mediaDescriptor: frameReader.mediaDescriptor, + link: { + name: frameReader["Lnk "]["Nm "], + fullPath: frameReader["Lnk "].fullPath, + relativePath: frameReader["Lnk "].relPath, + }, + }, + }) + ), + })); + } + }, + (writer, target) => { + const timeline = target.timelineInformation!; + const desc: TimelineInformationDescriptor = { + Vrsn: 1, + enab: timeline.enabled, + frameStep: timeline.frameStep, + frameRate: timeline.frameRate, + time: timeline.time, + duration: timeline.duration, + workInTime: timeline.workInTime, + workOutTime: timeline.workOutTime, + LCnt: timeline.repeats, + globalTrackList: serializeTrackList(timeline.globalTracks), + audioClipGroupList: { + audioClipGroupList: timeline.audioClipGroups?.map((a) => ({ + groupID: a.id, + muted: a.muted, + audioClipList: a.audioClips.map((c) => ({ + clipID: c.id, + timeScope: { + Vrsn: 1, + Strt: c.start, + duration: c.duration, + inTime: c.inTime, + outTime: c.outTime, + }, + frameReader: { + frameReaderType: c.frameReader.type, + descVersion: 1, + "Lnk ": { + descVersion: 1, + "Nm ": c.frameReader.link.name, + fullPath: c.frameReader.link.fullPath, + relPath: c.frameReader.link.relativePath, + }, + mediaDescriptor: c.frameReader.mediaDescriptor, + }, + muted: c.muted, + audioLevel: c.audioLevel, + })), + })), + }, + hasMotion: timeline.hasMotion, + }; + + writeVersionAndDescriptor(writer, "", "null", desc, "anim"); + } ); interface SheetDisclosureDescriptor { - Vrsn: 1; - sheetTimelineOptions?: { - Vrsn: 2; - sheetID: number; - sheetDisclosed: boolean; - lightsDisclosed: boolean; - meshesDisclosed: boolean; - materialsDisclosed: boolean; - }[]; + Vrsn: 1; + sheetTimelineOptions?: { + Vrsn: 2; + sheetID: number; + sheetDisclosed: boolean; + lightsDisclosed: boolean; + meshesDisclosed: boolean; + materialsDisclosed: boolean; + }[]; } addHandler( - 1076, // Sheet Disclosure - target => target.sheetDisclosure !== undefined, - (reader, target) => { - const desc = readVersionAndDescriptor(reader) as SheetDisclosureDescriptor; - - target.sheetDisclosure = {}; - - if (desc.sheetTimelineOptions) { - target.sheetDisclosure.sheetTimelineOptions = desc.sheetTimelineOptions.map(o => ({ - sheetID: o.sheetID, - sheetDisclosed: o.sheetDisclosed, - lightsDisclosed: o.lightsDisclosed, - meshesDisclosed: o.meshesDisclosed, - materialsDisclosed: o.materialsDisclosed, - })); - } - }, - (writer, target) => { - const disclosure = target.sheetDisclosure!; - const desc: SheetDisclosureDescriptor = { Vrsn: 1 }; - - if (disclosure.sheetTimelineOptions) { - desc.sheetTimelineOptions = disclosure.sheetTimelineOptions.map(d => ({ - Vrsn: 2, - sheetID: d.sheetID, - sheetDisclosed: d.sheetDisclosed, - lightsDisclosed: d.lightsDisclosed, - meshesDisclosed: d.meshesDisclosed, - materialsDisclosed: d.materialsDisclosed, - })); - } - - writeVersionAndDescriptor(writer, '', 'null', desc); - }, + 1076, // Sheet Disclosure + (target) => target.sheetDisclosure !== undefined, + async (reader, target) => { + const desc = readVersionAndDescriptor(reader) as SheetDisclosureDescriptor; + + target.sheetDisclosure = {}; + + if (desc.sheetTimelineOptions) { + target.sheetDisclosure.sheetTimelineOptions = + desc.sheetTimelineOptions.map((o) => ({ + sheetID: o.sheetID, + sheetDisclosed: o.sheetDisclosed, + lightsDisclosed: o.lightsDisclosed, + meshesDisclosed: o.meshesDisclosed, + materialsDisclosed: o.materialsDisclosed, + })); + } + }, + (writer, target) => { + const disclosure = target.sheetDisclosure!; + const desc: SheetDisclosureDescriptor = { Vrsn: 1 }; + + if (disclosure.sheetTimelineOptions) { + desc.sheetTimelineOptions = disclosure.sheetTimelineOptions.map((d) => ({ + Vrsn: 2, + sheetID: d.sheetID, + sheetDisclosed: d.sheetDisclosed, + lightsDisclosed: d.lightsDisclosed, + meshesDisclosed: d.meshesDisclosed, + materialsDisclosed: d.materialsDisclosed, + })); + } + + writeVersionAndDescriptor(writer, "", "null", desc); + } ); addHandler( - 1054, // URL List - target => target.urlsList !== undefined, - (reader, target, _, options) => { - const count = readUint32(reader); - target.urlsList = []; - - for (let i = 0; i < count; i++) { - const long = readSignature(reader); - if (long !== 'slic' && options.throwForMissingFeatures) throw new Error('Unknown long'); - const id = readUint32(reader); - const url = readUnicodeString(reader); - target.urlsList.push({ id, url, ref: 'slice' }); - } - }, - (writer, target) => { - const list = target.urlsList!; - writeUint32(writer, list.length); - - for (let i = 0; i < list.length; i++) { - writeSignature(writer, 'slic'); - writeUint32(writer, list[i].id); - writeUnicodeString(writer, list[i].url); - } - }, + 1054, // URL List + (target) => target.urlsList !== undefined, + async (reader, target, _, options) => { + const count = readUint32(reader); + target.urlsList = []; + + for (let i = 0; i < count; i++) { + const long = readSignature(reader); + if (long !== "slic" && options.throwForMissingFeatures) + throw new Error("Unknown long"); + const id = readUint32(reader); + const url = readUnicodeString(reader); + target.urlsList.push({ id, url, ref: "slice" }); + } + }, + (writer, target) => { + const list = target.urlsList!; + writeUint32(writer, list.length); + + for (let i = 0; i < list.length; i++) { + writeSignature(writer, "slic"); + writeUint32(writer, list[i].id); + writeUnicodeString(writer, list[i].url); + } + } ); interface BoundsDesc { - 'Top ': number; - Left: number; - Btom: number; - Rght: number; + "Top ": number; + Left: number; + Btom: number; + Rght: number; } interface SlicesSliceDesc { - sliceID: number; - groupID: number; - origin: string; - 'Nm '?: string; - Type: string; - bounds: BoundsDesc; - url: string; - null: string; - Msge: string; - altTag: string; - cellTextIsHTML: boolean; - cellText: string; - horzAlign: string; - vertAlign: string; - bgColorType: string; - bgColor?: { 'Rd ': number; 'Grn ': number; 'Bl ': number; alpha: number; }; - topOutset?: number; - leftOutset?: number; - bottomOutset?: number; - rightOutset?: number; + sliceID: number; + groupID: number; + origin: string; + "Nm "?: string; + Type: string; + bounds: BoundsDesc; + url: string; + null: string; + Msge: string; + altTag: string; + cellTextIsHTML: boolean; + cellText: string; + horzAlign: string; + vertAlign: string; + bgColorType: string; + bgColor?: { "Rd ": number; "Grn ": number; "Bl ": number; alpha: number }; + topOutset?: number; + leftOutset?: number; + bottomOutset?: number; + rightOutset?: number; } interface SlicesDesc { - bounds: BoundsDesc; - slices: SlicesSliceDesc[]; + bounds: BoundsDesc; + slices: SlicesSliceDesc[]; } interface SlicesDesc7 extends SlicesDesc { - baseName: string; + baseName: string; } -function boundsToBounds(bounds: { left: number; top: number; right: number; bottom: number }): BoundsDesc { - return { 'Top ': bounds.top, Left: bounds.left, Btom: bounds.bottom, Rght: bounds.right }; +function boundsToBounds(bounds: { + left: number; + top: number; + right: number; + bottom: number; +}): BoundsDesc { + return { + "Top ": bounds.top, + Left: bounds.left, + Btom: bounds.bottom, + Rght: bounds.right, + }; } -function boundsFromBounds(bounds: BoundsDesc): { left: number; top: number; right: number; bottom: number } { - return { top: bounds['Top '], left: bounds.Left, bottom: bounds.Btom, right: bounds.Rght }; +function boundsFromBounds(bounds: BoundsDesc): { + left: number; + top: number; + right: number; + bottom: number; +} { + return { + top: bounds["Top "], + left: bounds.Left, + bottom: bounds.Btom, + right: bounds.Rght, + }; } function clamped(array: T[], index: number) { - return array[Math.max(0, Math.min(array.length - 1, index))]; + return array[Math.max(0, Math.min(array.length - 1, index))]; } -const sliceOrigins: ('userGenerated' | 'autoGenerated' | 'layer')[] = ['autoGenerated', 'layer', 'userGenerated']; -const sliceTypes: ('image' | 'noImage')[] = ['noImage', 'image']; -const sliceAlignments: ('default')[] = ['default']; +const sliceOrigins: ("userGenerated" | "autoGenerated" | "layer")[] = [ + "autoGenerated", + "layer", + "userGenerated", +]; +const sliceTypes: ("image" | "noImage")[] = ["noImage", "image"]; +const sliceAlignments: "default"[] = ["default"]; addHandler( - 1050, // Slices - target => target.slices ? target.slices.length : 0, - (reader, target) => { - const version = readUint32(reader); - - if (version === 6) { - if (!target.slices) target.slices = []; - - const top = readInt32(reader); - const left = readInt32(reader); - const bottom = readInt32(reader); - const right = readInt32(reader); - const groupName = readUnicodeString(reader); - const count = readUint32(reader); - target.slices.push({ bounds: { top, left, bottom, right }, groupName, slices: [] }); - const slices = target.slices[target.slices.length - 1].slices; - - for (let i = 0; i < count; i++) { - const id = readUint32(reader); - const groupId = readUint32(reader); - const origin = clamped(sliceOrigins, readUint32(reader)); - const associatedLayerId = origin == 'layer' ? readUint32(reader) : 0; - const name = readUnicodeString(reader); - const type = clamped(sliceTypes, readUint32(reader)); - const left = readInt32(reader); - const top = readInt32(reader); - const right = readInt32(reader); - const bottom = readInt32(reader); - const url = readUnicodeString(reader); - const target = readUnicodeString(reader); - const message = readUnicodeString(reader); - const altTag = readUnicodeString(reader); - const cellTextIsHTML = !!readUint8(reader); - const cellText = readUnicodeString(reader); - const horizontalAlignment = clamped(sliceAlignments, readUint32(reader)); - const verticalAlignment = clamped(sliceAlignments, readUint32(reader)); - const a = readUint8(reader); - const r = readUint8(reader); - const g = readUint8(reader); - const b = readUint8(reader); - const backgroundColorType = ((a + r + g + b) === 0) ? 'none' : (a === 0 ? 'matte' : 'color'); - slices.push({ - id, groupId, origin, associatedLayerId, name, target, message, altTag, cellTextIsHTML, cellText, - horizontalAlignment, verticalAlignment, type, url, - bounds: { top, left, bottom, right }, - backgroundColorType, backgroundColor: { r, g, b, a }, - }); - } - const desc = readVersionAndDescriptor(reader) as SlicesDesc; - desc.slices.forEach(d => { - const slice = slices.find(s => d.sliceID == s.id); - if (slice) { - slice.topOutset = d.topOutset; - slice.leftOutset = d.leftOutset; - slice.bottomOutset = d.bottomOutset; - slice.rightOutset = d.rightOutset; - } - }); - } else if (version === 7 || version === 8) { - const desc = readVersionAndDescriptor(reader) as SlicesDesc7; - - if (!target.slices) target.slices = []; - target.slices.push({ - groupName: desc.baseName, - bounds: boundsFromBounds(desc.bounds), - slices: desc.slices.map(s => ({ - ...(s['Nm '] ? { name: s['Nm '] } : {}), - id: s.sliceID, - groupId: s.groupID, - associatedLayerId: 0, - origin: ESliceOrigin.decode(s.origin), - type: ESliceType.decode(s.Type), - bounds: boundsFromBounds(s.bounds), - url: s.url, - target: s.null, - message: s.Msge, - altTag: s.altTag, - cellTextIsHTML: s.cellTextIsHTML, - cellText: s.cellText, - horizontalAlignment: ESliceHorzAlign.decode(s.horzAlign), - verticalAlignment: ESliceVertAlign.decode(s.vertAlign), - backgroundColorType: ESliceBGColorType.decode(s.bgColorType), - backgroundColor: s.bgColor ? { r: s.bgColor['Rd '], g: s.bgColor['Grn '], b: s.bgColor['Bl '], a: s.bgColor.alpha } : { r: 0, g: 0, b: 0, a: 0 }, - topOutset: s.topOutset || 0, - leftOutset: s.leftOutset || 0, - bottomOutset: s.bottomOutset || 0, - rightOutset: s.rightOutset || 0, - })), - }); - } else { - throw new Error(`Invalid slices version (${version})`); - } - }, - (writer, target, index) => { - const { bounds, groupName, slices } = target.slices![index]; - - writeUint32(writer, 6); // version - writeInt32(writer, bounds.top); - writeInt32(writer, bounds.left); - writeInt32(writer, bounds.bottom); - writeInt32(writer, bounds.right); - writeUnicodeString(writer, groupName); - writeUint32(writer, slices.length); - - for (let i = 0; i < slices.length; i++) { - const slice = slices[i]; - let { a, r, g, b } = slice.backgroundColor; - - if (slice.backgroundColorType === 'none') { - a = r = g = b = 0; - } else if (slice.backgroundColorType === 'matte') { - a = 0; - r = g = b = 255; - } - - writeUint32(writer, slice.id); - writeUint32(writer, slice.groupId); - writeUint32(writer, sliceOrigins.indexOf(slice.origin)); - if (slice.origin === 'layer') writeUint32(writer, slice.associatedLayerId); - writeUnicodeString(writer, slice.name || ''); - writeUint32(writer, sliceTypes.indexOf(slice.type)); - writeInt32(writer, slice.bounds.left); - writeInt32(writer, slice.bounds.top); - writeInt32(writer, slice.bounds.right); - writeInt32(writer, slice.bounds.bottom); - writeUnicodeString(writer, slice.url); - writeUnicodeString(writer, slice.target); - writeUnicodeString(writer, slice.message); - writeUnicodeString(writer, slice.altTag); - writeUint8(writer, slice.cellTextIsHTML ? 1 : 0); - writeUnicodeString(writer, slice.cellText); - writeUint32(writer, sliceAlignments.indexOf(slice.horizontalAlignment)); - writeUint32(writer, sliceAlignments.indexOf(slice.verticalAlignment)); - writeUint8(writer, a); - writeUint8(writer, r); - writeUint8(writer, g); - writeUint8(writer, b); - } - - const desc: SlicesDesc = { - bounds: boundsToBounds(bounds), - slices: [], - }; - - slices.forEach(s => { - const slice: SlicesSliceDesc = { - sliceID: s.id, - groupID: s.groupId, - origin: ESliceOrigin.encode(s.origin), - Type: ESliceType.encode(s.type), - bounds: boundsToBounds(s.bounds), - ...(s.name ? { 'Nm ': s.name } : {}), - url: s.url, - null: s.target, - Msge: s.message, - altTag: s.altTag, - cellTextIsHTML: s.cellTextIsHTML, - cellText: s.cellText, - horzAlign: ESliceHorzAlign.encode(s.horizontalAlignment), - vertAlign: ESliceVertAlign.encode(s.verticalAlignment), - bgColorType: ESliceBGColorType.encode(s.backgroundColorType), - }; - - if (s.backgroundColorType === 'color') { - const { r, g, b, a } = s.backgroundColor; - slice.bgColor = { 'Rd ': r, 'Grn ': g, 'Bl ': b, alpha: a }; - } - - slice.topOutset = s.topOutset || 0; - slice.leftOutset = s.leftOutset || 0; - slice.bottomOutset = s.bottomOutset || 0; - slice.rightOutset = s.rightOutset || 0; - desc.slices.push(slice); - }); - - writeVersionAndDescriptor(writer, '', 'null', desc, 'slices'); - }, + 1050, // Slices + (target) => (target.slices ? target.slices.length : 0), + async (reader, target) => { + const version = readUint32(reader); + + if (version === 6) { + if (!target.slices) target.slices = []; + + const top = readInt32(reader); + const left = readInt32(reader); + const bottom = readInt32(reader); + const right = readInt32(reader); + const groupName = readUnicodeString(reader); + const count = readUint32(reader); + target.slices.push({ + bounds: { top, left, bottom, right }, + groupName, + slices: [], + }); + const slices = target.slices[target.slices.length - 1].slices; + + for (let i = 0; i < count; i++) { + const id = readUint32(reader); + const groupId = readUint32(reader); + const origin = clamped(sliceOrigins, readUint32(reader)); + const associatedLayerId = origin == "layer" ? readUint32(reader) : 0; + const name = readUnicodeString(reader); + const type = clamped(sliceTypes, readUint32(reader)); + const left = readInt32(reader); + const top = readInt32(reader); + const right = readInt32(reader); + const bottom = readInt32(reader); + const url = readUnicodeString(reader); + const target = readUnicodeString(reader); + const message = readUnicodeString(reader); + const altTag = readUnicodeString(reader); + const cellTextIsHTML = !!readUint8(reader); + const cellText = readUnicodeString(reader); + const horizontalAlignment = clamped( + sliceAlignments, + readUint32(reader) + ); + const verticalAlignment = clamped(sliceAlignments, readUint32(reader)); + const a = readUint8(reader); + const r = readUint8(reader); + const g = readUint8(reader); + const b = readUint8(reader); + const backgroundColorType = + a + r + g + b === 0 ? "none" : a === 0 ? "matte" : "color"; + slices.push({ + id, + groupId, + origin, + associatedLayerId, + name, + target, + message, + altTag, + cellTextIsHTML, + cellText, + horizontalAlignment, + verticalAlignment, + type, + url, + bounds: { top, left, bottom, right }, + backgroundColorType, + backgroundColor: { r, g, b, a }, + }); + } + const desc = readVersionAndDescriptor(reader) as SlicesDesc; + desc.slices.forEach((d) => { + const slice = slices.find((s) => d.sliceID == s.id); + if (slice) { + slice.topOutset = d.topOutset; + slice.leftOutset = d.leftOutset; + slice.bottomOutset = d.bottomOutset; + slice.rightOutset = d.rightOutset; + } + }); + } else if (version === 7 || version === 8) { + const desc = readVersionAndDescriptor(reader) as SlicesDesc7; + + if (!target.slices) target.slices = []; + target.slices.push({ + groupName: desc.baseName, + bounds: boundsFromBounds(desc.bounds), + slices: desc.slices.map((s) => ({ + ...(s["Nm "] ? { name: s["Nm "] } : {}), + id: s.sliceID, + groupId: s.groupID, + associatedLayerId: 0, + origin: ESliceOrigin.decode(s.origin), + type: ESliceType.decode(s.Type), + bounds: boundsFromBounds(s.bounds), + url: s.url, + target: s.null, + message: s.Msge, + altTag: s.altTag, + cellTextIsHTML: s.cellTextIsHTML, + cellText: s.cellText, + horizontalAlignment: ESliceHorzAlign.decode(s.horzAlign), + verticalAlignment: ESliceVertAlign.decode(s.vertAlign), + backgroundColorType: ESliceBGColorType.decode(s.bgColorType), + backgroundColor: s.bgColor + ? { + r: s.bgColor["Rd "], + g: s.bgColor["Grn "], + b: s.bgColor["Bl "], + a: s.bgColor.alpha, + } + : { r: 0, g: 0, b: 0, a: 0 }, + topOutset: s.topOutset || 0, + leftOutset: s.leftOutset || 0, + bottomOutset: s.bottomOutset || 0, + rightOutset: s.rightOutset || 0, + })), + }); + } else { + throw new Error(`Invalid slices version (${version})`); + } + }, + (writer, target, index) => { + const { bounds, groupName, slices } = target.slices![index]; + + writeUint32(writer, 6); // version + writeInt32(writer, bounds.top); + writeInt32(writer, bounds.left); + writeInt32(writer, bounds.bottom); + writeInt32(writer, bounds.right); + writeUnicodeString(writer, groupName); + writeUint32(writer, slices.length); + + for (let i = 0; i < slices.length; i++) { + const slice = slices[i]; + let { a, r, g, b } = slice.backgroundColor; + + if (slice.backgroundColorType === "none") { + a = r = g = b = 0; + } else if (slice.backgroundColorType === "matte") { + a = 0; + r = g = b = 255; + } + + writeUint32(writer, slice.id); + writeUint32(writer, slice.groupId); + writeUint32(writer, sliceOrigins.indexOf(slice.origin)); + if (slice.origin === "layer") + writeUint32(writer, slice.associatedLayerId); + writeUnicodeString(writer, slice.name || ""); + writeUint32(writer, sliceTypes.indexOf(slice.type)); + writeInt32(writer, slice.bounds.left); + writeInt32(writer, slice.bounds.top); + writeInt32(writer, slice.bounds.right); + writeInt32(writer, slice.bounds.bottom); + writeUnicodeString(writer, slice.url); + writeUnicodeString(writer, slice.target); + writeUnicodeString(writer, slice.message); + writeUnicodeString(writer, slice.altTag); + writeUint8(writer, slice.cellTextIsHTML ? 1 : 0); + writeUnicodeString(writer, slice.cellText); + writeUint32(writer, sliceAlignments.indexOf(slice.horizontalAlignment)); + writeUint32(writer, sliceAlignments.indexOf(slice.verticalAlignment)); + writeUint8(writer, a); + writeUint8(writer, r); + writeUint8(writer, g); + writeUint8(writer, b); + } + + const desc: SlicesDesc = { + bounds: boundsToBounds(bounds), + slices: [], + }; + + slices.forEach((s) => { + const slice: SlicesSliceDesc = { + sliceID: s.id, + groupID: s.groupId, + origin: ESliceOrigin.encode(s.origin), + Type: ESliceType.encode(s.type), + bounds: boundsToBounds(s.bounds), + ...(s.name ? { "Nm ": s.name } : {}), + url: s.url, + null: s.target, + Msge: s.message, + altTag: s.altTag, + cellTextIsHTML: s.cellTextIsHTML, + cellText: s.cellText, + horzAlign: ESliceHorzAlign.encode(s.horizontalAlignment), + vertAlign: ESliceVertAlign.encode(s.verticalAlignment), + bgColorType: ESliceBGColorType.encode(s.backgroundColorType), + }; + + if (s.backgroundColorType === "color") { + const { r, g, b, a } = s.backgroundColor; + slice.bgColor = { "Rd ": r, "Grn ": g, "Bl ": b, alpha: a }; + } + + slice.topOutset = s.topOutset || 0; + slice.leftOutset = s.leftOutset || 0; + slice.bottomOutset = s.bottomOutset || 0; + slice.rightOutset = s.rightOutset || 0; + desc.slices.push(slice); + }); + + writeVersionAndDescriptor(writer, "", "null", desc, "slices"); + } ); addHandler( - 1064, - target => target.pixelAspectRatio !== undefined, - (reader, target) => { - if (readUint32(reader) > 2) throw new Error('Invalid pixelAspectRatio version'); - target.pixelAspectRatio = { aspect: readFloat64(reader) }; - }, - (writer, target) => { - writeUint32(writer, 2); // version - writeFloat64(writer, target.pixelAspectRatio!.aspect); - }, + 1064, + (target) => target.pixelAspectRatio !== undefined, + async (reader, target) => { + if (readUint32(reader) > 2) + throw new Error("Invalid pixelAspectRatio version"); + target.pixelAspectRatio = { aspect: readFloat64(reader) }; + }, + (writer, target) => { + writeUint32(writer, 2); // version + writeFloat64(writer, target.pixelAspectRatio!.aspect); + } ); addHandler( - 1041, - target => target.iccUntaggedProfile !== undefined, - (reader, target) => { - target.iccUntaggedProfile = !!readUint8(reader); - }, - (writer, target) => { - writeUint8(writer, target.iccUntaggedProfile ? 1 : 0); - }, + 1041, + (target) => target.iccUntaggedProfile !== undefined, + async (reader, target) => { + target.iccUntaggedProfile = !!readUint8(reader); + }, + (writer, target) => { + writeUint8(writer, target.iccUntaggedProfile ? 1 : 0); + } ); -MOCK_HANDLERS && addHandler( - 1039, // ICC Profile - target => (target as any)._ir1039 !== undefined, - (reader, target, left) => { - // TODO: this is raw bytes, just return as a byte array - LOG_MOCK_HANDLERS && console.log('image resource 1039', left()); - (target as any)._ir1039 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1039); - }, -); +MOCK_HANDLERS && + addHandler( + 1039, // ICC Profile + (target) => (target as any)._ir1039 !== undefined, + async (reader, target, left) => { + // TODO: this is raw bytes, just return as a byte array + LOG_MOCK_HANDLERS && console.log("image resource 1039", await left()); + (target as any)._ir1039 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1039); + } + ); addHandler( - 1044, - target => target.idsSeedNumber !== undefined, - (reader, target) => target.idsSeedNumber = readUint32(reader), - (writer, target) => writeUint32(writer, target.idsSeedNumber!), + 1044, + (target) => target.idsSeedNumber !== undefined, + async (reader, target) => { + target.idsSeedNumber = readUint32(reader); + }, + (writer, target) => writeUint32(writer, target.idsSeedNumber!) ); addHandler( - 1036, - target => target.thumbnail !== undefined || target.thumbnailRaw !== undefined, - (reader, target, left, options) => { - const format = readUint32(reader); // 1 = kJpegRGB, 0 = kRawRGB - const width = readUint32(reader); - const height = readUint32(reader); - readUint32(reader); // widthBytes = (width * bits_per_pixel + 31) / 32 * 4. - readUint32(reader); // totalSize = widthBytes * height * planes - readUint32(reader); // sizeAfterCompression - const bitsPerPixel = readUint16(reader); // 24 - const planes = readUint16(reader); // 1 - - if (format !== 1 || bitsPerPixel !== 24 || planes !== 1) { - options.logMissingFeatures && console.log(`Invalid thumbnail data (format: ${format}, bitsPerPixel: ${bitsPerPixel}, planes: ${planes})`); - skipBytes(reader, left()); - return; - } - - const size = left(); - const data = readBytes(reader, size); - - if (options.useRawThumbnail) { - target.thumbnailRaw = { width, height, data }; - } else if (data.byteLength) { - target.thumbnail = createCanvasFromData(data); - } - }, - (writer, target) => { - let width = 0; - let height = 0; - let data: Uint8Array; - - if (target.thumbnailRaw) { - width = target.thumbnailRaw.width; - height = target.thumbnailRaw.height; - data = target.thumbnailRaw.data; - } else { - const dataUrl = target.thumbnail!.toDataURL('image/jpeg', 1)?.substring('data:image/jpeg;base64,'.length); - - if (dataUrl) { - width = target.thumbnail!.width; - height = target.thumbnail!.height; - data = toByteArray(dataUrl); - } else { - data = new Uint8Array(0); - } - } - - const bitsPerPixel = 24; - const widthBytes = Math.floor((width * bitsPerPixel + 31) / 32) * 4; - const planes = 1; - const totalSize = widthBytes * height * planes; - const sizeAfterCompression = data.length; - - writeUint32(writer, 1); // 1 = kJpegRGB - writeUint32(writer, width); - writeUint32(writer, height); - writeUint32(writer, widthBytes); - writeUint32(writer, totalSize); - writeUint32(writer, sizeAfterCompression); - writeUint16(writer, bitsPerPixel); - writeUint16(writer, planes); - writeBytes(writer, data); - }, + 1036, + (target) => + target.thumbnail !== undefined || target.thumbnailRaw !== undefined, + async (reader, target, left, options) => { + const format = readUint32(reader); // 1 = kJpegRGB, 0 = kRawRGB + const width = readUint32(reader); + const height = readUint32(reader); + readUint32(reader); // widthBytes = (width * bits_per_pixel + 31) / 32 * 4. + readUint32(reader); // totalSize = widthBytes * height * planes + readUint32(reader); // sizeAfterCompression + const bitsPerPixel = readUint16(reader); // 24 + const planes = readUint16(reader); // 1 + + if (format !== 1 || bitsPerPixel !== 24 || planes !== 1) { + options.logMissingFeatures && + console.log( + `Invalid thumbnail data (format: ${format}, bitsPerPixel: ${bitsPerPixel}, planes: ${planes})` + ); + skipBytes(reader, await left()); + return; + } + + const size = await left(); + const data = readBytes(reader, size); + + if (options.useRawThumbnail) { + target.thumbnailRaw = { width, height, data }; + } else if (data.byteLength) { + target.thumbnail = createCanvasFromData(data); + } + }, + (writer, target) => { + let width = 0; + let height = 0; + let data: Uint8Array; + + if (target.thumbnailRaw) { + width = target.thumbnailRaw.width; + height = target.thumbnailRaw.height; + data = target.thumbnailRaw.data; + } else { + const dataUrl = target + .thumbnail!.toDataURL("image/jpeg", 1) + ?.substring("data:image/jpeg;base64,".length); + + if (dataUrl) { + width = target.thumbnail!.width; + height = target.thumbnail!.height; + data = toByteArray(dataUrl); + } else { + data = new Uint8Array(0); + } + } + + const bitsPerPixel = 24; + const widthBytes = Math.floor((width * bitsPerPixel + 31) / 32) * 4; + const planes = 1; + const totalSize = widthBytes * height * planes; + const sizeAfterCompression = data.length; + + writeUint32(writer, 1); // 1 = kJpegRGB + writeUint32(writer, width); + writeUint32(writer, height); + writeUint32(writer, widthBytes); + writeUint32(writer, totalSize); + writeUint32(writer, sizeAfterCompression); + writeUint16(writer, bitsPerPixel); + writeUint16(writer, planes); + writeBytes(writer, data); + } ); addHandler( - 1057, - target => target.versionInfo !== undefined, - (reader, target, left) => { - const version = readUint32(reader); - if (version !== 1) throw new Error('Invalid versionInfo version'); - - target.versionInfo = { - hasRealMergedData: !!readUint8(reader), - writerName: readUnicodeString(reader), - readerName: readUnicodeString(reader), - fileVersion: readUint32(reader), - }; - - skipBytes(reader, left()); - }, - (writer, target) => { - const versionInfo = target.versionInfo!; - writeUint32(writer, 1); // version - writeUint8(writer, versionInfo.hasRealMergedData ? 1 : 0); - writeUnicodeString(writer, versionInfo.writerName); - writeUnicodeString(writer, versionInfo.readerName); - writeUint32(writer, versionInfo.fileVersion); - }, + 1057, + (target) => target.versionInfo !== undefined, + async (reader, target, left) => { + const version = readUint32(reader); + if (version !== 1) throw new Error("Invalid versionInfo version"); + + target.versionInfo = { + hasRealMergedData: !!readUint8(reader), + writerName: readUnicodeString(reader), + readerName: readUnicodeString(reader), + fileVersion: readUint32(reader), + }; + + skipBytes(reader, await left()); + }, + (writer, target) => { + const versionInfo = target.versionInfo!; + writeUint32(writer, 1); // version + writeUint8(writer, versionInfo.hasRealMergedData ? 1 : 0); + writeUnicodeString(writer, versionInfo.writerName); + writeUnicodeString(writer, versionInfo.readerName); + writeUint32(writer, versionInfo.fileVersion); + } ); -MOCK_HANDLERS && addHandler( - 1058, // EXIF data 1. - target => (target as any)._ir1058 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1058', left()); - (target as any)._ir1058 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1058); - }, -); +MOCK_HANDLERS && + addHandler( + 1058, // EXIF data 1. + (target) => (target as any)._ir1058 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1058", await left()); + (target as any)._ir1058 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1058); + } + ); addHandler( - 7000, - target => target.imageReadyVariables !== undefined, - (reader, target, left) => { - target.imageReadyVariables = readUtf8String(reader, left()); - }, - (writer, target) => { - writeUtf8String(writer, target.imageReadyVariables!); - }, + 7000, + (target) => target.imageReadyVariables !== undefined, + async (reader, target, left) => { + target.imageReadyVariables = readUtf8String(reader, await left()); + }, + (writer, target) => { + writeUtf8String(writer, target.imageReadyVariables!); + } ); addHandler( - 7001, - target => target.imageReadyDataSets !== undefined, - (reader, target, left) => { - target.imageReadyDataSets = readUtf8String(reader, left()); - }, - (writer, target) => { - writeUtf8String(writer, target.imageReadyDataSets!); - }, + 7001, + (target) => target.imageReadyDataSets !== undefined, + async (reader, target, left) => { + target.imageReadyDataSets = readUtf8String(reader, await left()); + }, + (writer, target) => { + writeUtf8String(writer, target.imageReadyDataSets!); + } ); interface Descriptor1088 { - 'null': string[]; + null: string[]; } addHandler( - 1088, - target => target.pathSelectionState !== undefined, - (reader, target, _left) => { - const desc: Descriptor1088 = readVersionAndDescriptor(reader); - target.pathSelectionState = desc['null']; - }, - (writer, target) => { - const desc: Descriptor1088 = { 'null': target.pathSelectionState! }; - writeVersionAndDescriptor(writer, '', 'null', desc); - }, + 1088, + (target) => target.pathSelectionState !== undefined, + async (reader, target, _left) => { + const desc: Descriptor1088 = readVersionAndDescriptor(reader); + target.pathSelectionState = desc["null"]; + }, + (writer, target) => { + const desc: Descriptor1088 = { null: target.pathSelectionState! }; + writeVersionAndDescriptor(writer, "", "null", desc); + } ); -MOCK_HANDLERS && addHandler( - 1025, - target => (target as any)._ir1025 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 1025', left()); - (target as any)._ir1025 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir1025); - }, -); - -const FrmD = createEnum<'auto' | 'none' | 'dispose'>('FrmD', '', { - auto: 'Auto', - none: 'None', - dispose: 'Disp', +MOCK_HANDLERS && + addHandler( + 1025, + (target) => (target as any)._ir1025 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 1025", await left()); + (target as any)._ir1025 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir1025); + } + ); + +const FrmD = createEnum<"auto" | "none" | "dispose">("FrmD", "", { + auto: "Auto", + none: "None", + dispose: "Disp", }); interface AnimationFrameDescriptor { - FrID: number; - FrDl?: number; - FrDs: string; - FrGA?: number; + FrID: number; + FrDl?: number; + FrDs: string; + FrGA?: number; } interface AnimationDescriptor { - FsID: number; - AFrm?: number; - FsFr: number[]; - LCnt: number; + FsID: number; + AFrm?: number; + FsFr: number[]; + LCnt: number; } interface AnimationsDescriptor { - AFSt?: number; - FrIn: AnimationFrameDescriptor[]; - FSts: AnimationDescriptor[]; + AFSt?: number; + FrIn: AnimationFrameDescriptor[]; + FSts: AnimationDescriptor[]; } addHandler( - 4000, // Plug-In resource(s) - target => target.animations !== undefined, - (reader, target, left, { logMissingFeatures, logDevFeatures }) => { - const key = readSignature(reader); - - if (key === 'mani') { - checkSignature(reader, 'IRFR'); - readSection(reader, 1, left => { - while (left() > 0) { - checkSignature(reader, '8BIM'); - const key = readSignature(reader); - - readSection(reader, 1, left => { - if (key === 'AnDs') { - const desc = readVersionAndDescriptor(reader) as AnimationsDescriptor; - target.animations = { - // desc.AFSt ??? - frames: desc.FrIn.map(x => ({ - id: x.FrID, - delay: (x.FrDl || 0) / 100, - dispose: x.FrDs ? FrmD.decode(x.FrDs) : 'auto', // missing == auto - // x.FrGA ??? - })), - animations: desc.FSts.map(x => ({ - id: x.FsID, - frames: x.FsFr, - repeats: x.LCnt, - activeFrame: x.AFrm || 0, - })), - }; - - // console.log('#4000 AnDs', require('util').inspect(desc, false, 99, true)); - // console.log('#4000 AnDs:result', require('util').inspect(target.animations, false, 99, true)); - } else if (key === 'Roll') { - const bytes = readBytes(reader, left()); - logDevFeatures && console.log('#4000 Roll', bytes); - } else { - logMissingFeatures && console.log('Unhandled subsection in #4000', key); - } - }); - } - }); - } else if (key === 'mopt') { - const bytes = readBytes(reader, left()); - logDevFeatures && console.log('#4000 mopt', bytes); - } else { - logMissingFeatures && console.log('Unhandled key in #4000:', key); - } - }, - (writer, target) => { - if (target.animations) { - writeSignature(writer, 'mani'); - writeSignature(writer, 'IRFR'); - writeSection(writer, 1, () => { - writeSignature(writer, '8BIM'); - writeSignature(writer, 'AnDs'); - writeSection(writer, 1, () => { - const desc: AnimationsDescriptor = { - // AFSt: 0, // ??? - FrIn: [], - FSts: [], - }; - - for (let i = 0; i < target.animations!.frames.length; i++) { - const f = target.animations!.frames[i]; - const frame: AnimationFrameDescriptor = { - FrID: f.id, - } as any; - if (f.delay) frame.FrDl = (f.delay * 100) | 0; - frame.FrDs = FrmD.encode(f.dispose); - // if (i === 0) frame.FrGA = 30; // ??? - desc.FrIn.push(frame); - } - - for (let i = 0; i < target.animations!.animations.length; i++) { - const a = target.animations!.animations[i]; - const anim: AnimationDescriptor = { - FsID: a.id, - AFrm: a.activeFrame! | 0, - FsFr: a.frames, - LCnt: a.repeats! | 0, - }; - desc.FSts.push(anim); - } - - writeVersionAndDescriptor(writer, '', 'null', desc); - }); - - // writeSignature(writer, '8BIM'); - // writeSignature(writer, 'Roll'); - // writeSection(writer, 1, () => { - // writeZeros(writer, 8); - // }); - }); - } - }, + 4000, // Plug-In resource(s) + (target) => target.animations !== undefined, + async (reader, target, left, { logMissingFeatures, logDevFeatures }) => { + const key = readSignature(reader); + + if (key === "mani") { + checkSignature(reader, "IRFR"); + await readSection(reader, 1, async (left) => { + while ((await left()) > 0) { + checkSignature(reader, "8BIM"); + const key = readSignature(reader); + + await readSection(reader, 1, async (left) => { + if (key === "AnDs") { + const desc = readVersionAndDescriptor( + reader + ) as AnimationsDescriptor; + target.animations = { + // desc.AFSt ??? + frames: desc.FrIn.map((x) => ({ + id: x.FrID, + delay: (x.FrDl || 0) / 100, + dispose: x.FrDs ? FrmD.decode(x.FrDs) : "auto", // missing == auto + // x.FrGA ??? + })), + animations: desc.FSts.map((x) => ({ + id: x.FsID, + frames: x.FsFr, + repeats: x.LCnt, + activeFrame: x.AFrm || 0, + })), + }; + + // console.log('#4000 AnDs', require('util').inspect(desc, false, 99, true)); + // console.log('#4000 AnDs:result', require('util').inspect(target.animations, false, 99, true)); + } else if (key === "Roll") { + const bytes = readBytes(reader, await left()); + logDevFeatures && console.log("#4000 Roll", bytes); + } else { + logMissingFeatures && + console.log("Unhandled subsection in #4000", key); + } + }); + } + }); + } else if (key === "mopt") { + const bytes = readBytes(reader, await left()); + logDevFeatures && console.log("#4000 mopt", bytes); + } else { + logMissingFeatures && console.log("Unhandled key in #4000:", key); + } + }, + (writer, target) => { + if (target.animations) { + writeSignature(writer, "mani"); + writeSignature(writer, "IRFR"); + writeSection(writer, 1, () => { + writeSignature(writer, "8BIM"); + writeSignature(writer, "AnDs"); + writeSection(writer, 1, () => { + const desc: AnimationsDescriptor = { + // AFSt: 0, // ??? + FrIn: [], + FSts: [], + }; + + for (let i = 0; i < target.animations!.frames.length; i++) { + const f = target.animations!.frames[i]; + const frame: AnimationFrameDescriptor = { + FrID: f.id, + } as any; + if (f.delay) frame.FrDl = (f.delay * 100) | 0; + frame.FrDs = FrmD.encode(f.dispose); + // if (i === 0) frame.FrGA = 30; // ??? + desc.FrIn.push(frame); + } + + for (let i = 0; i < target.animations!.animations.length; i++) { + const a = target.animations!.animations[i]; + const anim: AnimationDescriptor = { + FsID: a.id, + AFrm: a.activeFrame! | 0, + FsFr: a.frames, + LCnt: a.repeats! | 0, + }; + desc.FSts.push(anim); + } + + writeVersionAndDescriptor(writer, "", "null", desc); + }); + + // writeSignature(writer, '8BIM'); + // writeSignature(writer, 'Roll'); + // writeSection(writer, 1, () => { + // writeZeros(writer, 8); + // }); + }); + } + } ); // TODO: Unfinished -MOCK_HANDLERS && addHandler( - 4001, // Plug-In resource(s) - target => (target as any)._ir4001 !== undefined, - (reader, target, left, { logMissingFeatures, logDevFeatures }) => { - if (MOCK_HANDLERS) { - LOG_MOCK_HANDLERS && console.log('image resource 4001', left()); - (target as any)._ir4001 = readBytes(reader, left()); - return; - } - - const key = readSignature(reader); - - if (key === 'mfri') { - const version = readUint32(reader); - if (version !== 2) throw new Error('Invalid mfri version'); - - const length = readUint32(reader); - const bytes = readBytes(reader, length); - logDevFeatures && console.log('mfri', bytes); - } else if (key === 'mset') { - const desc = readVersionAndDescriptor(reader); - logDevFeatures && console.log('mset', desc); - } else { - logMissingFeatures && console.log('Unhandled key in #4001', key); - } - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir4001); - }, -); +MOCK_HANDLERS && + addHandler( + 4001, // Plug-In resource(s) + (target) => (target as any)._ir4001 !== undefined, + async (reader, target, left, { logMissingFeatures, logDevFeatures }) => { + if (MOCK_HANDLERS) { + LOG_MOCK_HANDLERS && console.log("image resource 4001", await left()); + (target as any)._ir4001 = readBytes(reader, await left()); + return; + } + + const key = readSignature(reader); + + if (key === "mfri") { + const version = readUint32(reader); + if (version !== 2) throw new Error("Invalid mfri version"); + + const length = readUint32(reader); + const bytes = readBytes(reader, length); + logDevFeatures && console.log("mfri", bytes); + } else if (key === "mset") { + const desc = readVersionAndDescriptor(reader); + logDevFeatures && console.log("mset", desc); + } else { + logMissingFeatures && console.log("Unhandled key in #4001", key); + } + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir4001); + } + ); // TODO: Unfinished -MOCK_HANDLERS && addHandler( - 4002, // Plug-In resource(s) - target => (target as any)._ir4002 !== undefined, - (reader, target, left) => { - LOG_MOCK_HANDLERS && console.log('image resource 4002', left()); - (target as any)._ir4002 = readBytes(reader, left()); - }, - (writer, target) => { - writeBytes(writer, (target as any)._ir4002); - }, -); +MOCK_HANDLERS && + addHandler( + 4002, // Plug-In resource(s) + (target) => (target as any)._ir4002 !== undefined, + async (reader, target, left) => { + LOG_MOCK_HANDLERS && console.log("image resource 4002", await left()); + (target as any)._ir4002 = readBytes(reader, await left()); + }, + (writer, target) => { + writeBytes(writer, (target as any)._ir4002); + } + ); diff --git a/src/index.ts b/src/index.ts index f7419dc..5fea689 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,62 @@ -import { Psd, ReadOptions, WriteOptions } from './psd'; -import { PsdWriter, writePsd as writePsdInternal, getWriterBuffer, createWriter, getWriterBufferNoCopy } from './psdWriter'; -import { PsdReader, readPsd as readPsdInternal, createReader } from './psdReader'; -export * from './abr'; -export * from './csh'; -export { initializeCanvas } from './helpers'; -export * from './psd'; -import { fromByteArray } from 'base64-js'; +import { PostImageDataHandler, Psd, ReadOptions, WriteOptions } from "./psd"; +import { + PsdWriter, + writePsd as writePsdInternal, + getWriterBuffer, + createWriter, + getWriterBufferNoCopy, +} from "./psdWriter"; +import { + PsdReader, + readPsd as readPsdInternal, + createReader, +} from "./psdReader"; +export * from "./abr"; +export * from "./csh"; +export { initializeCanvas, imageDataToCanvas } from "./helpers"; +export * from "./psd"; +import { fromByteArray } from "base64-js"; export { PsdReader, PsdWriter }; interface BufferLike { - buffer: ArrayBuffer; - byteOffset: number; - byteLength: number; + buffer: ArrayBuffer; + byteOffset: number; + byteLength: number; } export const byteArrayToBase64 = fromByteArray; -export function readPsd(buffer: ArrayBuffer | BufferLike, options?: ReadOptions): Psd { - const reader = 'buffer' in buffer ? - createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength) : - createReader(buffer); - return readPsdInternal(reader, options); +export async function readPsd( + buffer: ArrayBuffer | BufferLike, + options?: ReadOptions, + handler?: PostImageDataHandler +): Promise { + const reader = + "buffer" in buffer + ? createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength) + : createReader(buffer); + return readPsdInternal(reader, options, handler); } export function writePsd(psd: Psd, options?: WriteOptions): ArrayBuffer { - const writer = createWriter(); - writePsdInternal(writer, psd, options); - return getWriterBuffer(writer); + const writer = createWriter(); + writePsdInternal(writer, psd, options); + return getWriterBuffer(writer); } -export function writePsdUint8Array(psd: Psd, options?: WriteOptions): Uint8Array { - const writer = createWriter(); - writePsdInternal(writer, psd, options); - return getWriterBufferNoCopy(writer); +export function writePsdUint8Array( + psd: Psd, + options?: WriteOptions +): Uint8Array { + const writer = createWriter(); + writePsdInternal(writer, psd, options); + return getWriterBufferNoCopy(writer); } export function writePsdBuffer(psd: Psd, options?: WriteOptions): Buffer { - if (typeof Buffer === 'undefined') { - throw new Error('Buffer not supported on this platform'); - } + if (typeof Buffer === "undefined") { + throw new Error("Buffer not supported on this platform"); + } - return Buffer.from(writePsdUint8Array(psd, options)); + return Buffer.from(writePsdUint8Array(psd, options)); } diff --git a/src/psd.ts b/src/psd.ts index 3ea1b11..edee808 100644 --- a/src/psd.ts +++ b/src/psd.ts @@ -1,900 +1,1032 @@ -export type BlendMode = 'pass through' | 'normal' | 'dissolve' | 'darken' | 'multiply' | - 'color burn' | 'linear burn' | 'darker color' | 'lighten' | 'screen' | 'color dodge' | - 'linear dodge' | 'lighter color' | 'overlay' | 'soft light' | 'hard light' | - 'vivid light' | 'linear light' | 'pin light' | 'hard mix' | 'difference' | 'exclusion' | - 'subtract' | 'divide' | 'hue' | 'saturation' | 'color' | 'luminosity'; +export type BlendMode = + | "pass through" + | "normal" + | "dissolve" + | "darken" + | "multiply" + | "color burn" + | "linear burn" + | "darker color" + | "lighten" + | "screen" + | "color dodge" + | "linear dodge" + | "lighter color" + | "overlay" + | "soft light" + | "hard light" + | "vivid light" + | "linear light" + | "pin light" + | "hard mix" + | "difference" + | "exclusion" + | "subtract" + | "divide" + | "hue" + | "saturation" + | "color" + | "luminosity"; export const enum ColorMode { - Bitmap = 0, - Grayscale = 1, - Indexed = 2, - RGB = 3, - CMYK = 4, - Multichannel = 7, - Duotone = 8, - Lab = 9, + Bitmap = 0, + Grayscale = 1, + Indexed = 2, + RGB = 3, + CMYK = 4, + Multichannel = 7, + Duotone = 8, + Lab = 9, } export const enum SectionDividerType { - Other = 0, - OpenFolder = 1, - ClosedFolder = 2, - BoundingSectionDivider = 3, -} - -export type RGBA = { r: number; g: number; b: number; a: number; }; // values from 0 to 255 -export type RGB = { r: number; g: number; b: number; }; // values from 0 to 255 -export type FRGB = { fr: number; fg: number; fb: number; }; // values from 0 to 1 (can be above 1, can be negative) -export type HSB = { h: number; s: number; b: number; }; // values from 0 to 1 -export type CMYK = { c: number; m: number; y: number; k: number; }; // values from 0 to 255 -export type LAB = { l: number; a: number; b: number; }; // values `l` from 0 to 1; `a` and `b` from -1 to 1 + Other = 0, + OpenFolder = 1, + ClosedFolder = 2, + BoundingSectionDivider = 3, +} + +export type RGBA = { r: number; g: number; b: number; a: number }; // values from 0 to 255 +export type RGB = { r: number; g: number; b: number }; // values from 0 to 255 +export type FRGB = { fr: number; fg: number; fb: number }; // values from 0 to 1 (can be above 1, can be negative) +export type HSB = { h: number; s: number; b: number }; // values from 0 to 1 +export type CMYK = { c: number; m: number; y: number; k: number }; // values from 0 to 255 +export type LAB = { l: number; a: number; b: number }; // values `l` from 0 to 1; `a` and `b` from -1 to 1 export type Grayscale = { k: number }; // values from 0 to 255 export type Color = RGBA | RGB | FRGB | HSB | CMYK | LAB | Grayscale; export interface EffectContour { - name: string; - curve: { x: number; y: number; }[]; + name: string; + curve: { x: number; y: number }[]; } export interface EffectPattern { - name: string; - id: string; - // TODO: add fields + name: string; + id: string; + // TODO: add fields } export interface LayerEffectShadow { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - size?: UnitsValue; - angle?: number; - distance?: UnitsValue; - color?: Color; - blendMode?: BlendMode; - opacity?: number; - useGlobalLight?: boolean; - antialiased?: boolean; - contour?: EffectContour; - choke?: UnitsValue; // spread - layerConceals?: boolean; // only drop shadow + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + size?: UnitsValue; + angle?: number; + distance?: UnitsValue; + color?: Color; + blendMode?: BlendMode; + opacity?: number; + useGlobalLight?: boolean; + antialiased?: boolean; + contour?: EffectContour; + choke?: UnitsValue; // spread + layerConceals?: boolean; // only drop shadow } export interface LayerEffectsOuterGlow { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - size?: UnitsValue; - color?: Color; - blendMode?: BlendMode; - opacity?: number; - source?: GlowSource; - antialiased?: boolean; - noise?: number; - range?: number; - choke?: UnitsValue; - jitter?: number; - contour?: EffectContour; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + size?: UnitsValue; + color?: Color; + blendMode?: BlendMode; + opacity?: number; + source?: GlowSource; + antialiased?: boolean; + noise?: number; + range?: number; + choke?: UnitsValue; + jitter?: number; + contour?: EffectContour; } export interface LayerEffectInnerGlow { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - size?: UnitsValue; - color?: Color; - blendMode?: BlendMode; - opacity?: number; - source?: GlowSource; - technique?: GlowTechnique; - antialiased?: boolean; - noise?: number; - range?: number; - choke?: UnitsValue; // spread - jitter?: number; - contour?: EffectContour; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + size?: UnitsValue; + color?: Color; + blendMode?: BlendMode; + opacity?: number; + source?: GlowSource; + technique?: GlowTechnique; + antialiased?: boolean; + noise?: number; + range?: number; + choke?: UnitsValue; // spread + jitter?: number; + contour?: EffectContour; } export interface LayerEffectBevel { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - size?: UnitsValue; - angle?: number; - strength?: number; // depth - highlightBlendMode?: BlendMode; - shadowBlendMode?: BlendMode; - highlightColor?: Color; - shadowColor?: Color; - style?: BevelStyle; - highlightOpacity?: number; - shadowOpacity?: number; - soften?: UnitsValue; - useGlobalLight?: boolean; - altitude?: number; - technique?: BevelTechnique; - direction?: BevelDirection; - useTexture?: boolean; - useShape?: boolean; - antialiasGloss?: boolean; - contour?: EffectContour; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + size?: UnitsValue; + angle?: number; + strength?: number; // depth + highlightBlendMode?: BlendMode; + shadowBlendMode?: BlendMode; + highlightColor?: Color; + shadowColor?: Color; + style?: BevelStyle; + highlightOpacity?: number; + shadowOpacity?: number; + soften?: UnitsValue; + useGlobalLight?: boolean; + altitude?: number; + technique?: BevelTechnique; + direction?: BevelDirection; + useTexture?: boolean; + useShape?: boolean; + antialiasGloss?: boolean; + contour?: EffectContour; } export interface LayerEffectSolidFill { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - blendMode?: BlendMode; - color?: Color; - opacity?: number; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + blendMode?: BlendMode; + color?: Color; + opacity?: number; } export interface LayerEffectStroke { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - overprint?: boolean; - size?: UnitsValue; - position?: 'inside' | 'center' | 'outside'; - fillType?: 'color' | 'gradient' | 'pattern'; - blendMode?: BlendMode; - opacity?: number; - color?: Color; - gradient?: (EffectSolidGradient | EffectNoiseGradient) & ExtraGradientInfo; - pattern?: EffectPattern & {}; // TODO: additional pattern info + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + overprint?: boolean; + size?: UnitsValue; + position?: "inside" | "center" | "outside"; + fillType?: "color" | "gradient" | "pattern"; + blendMode?: BlendMode; + opacity?: number; + color?: Color; + gradient?: (EffectSolidGradient | EffectNoiseGradient) & ExtraGradientInfo; + pattern?: EffectPattern & {}; // TODO: additional pattern info } export interface LayerEffectSatin { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - size?: UnitsValue; - blendMode?: BlendMode; - color?: Color; - antialiased?: boolean; - opacity?: number; - distance?: UnitsValue; - invert?: boolean; - angle?: number; - contour?: EffectContour; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + size?: UnitsValue; + blendMode?: BlendMode; + color?: Color; + antialiased?: boolean; + opacity?: number; + distance?: UnitsValue; + invert?: boolean; + angle?: number; + contour?: EffectContour; } // not supported yet because of `Patt` section not implemented export interface LayerEffectPatternOverlay { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - blendMode?: BlendMode; - opacity?: number; - scale?: number; - pattern?: EffectPattern; - phase?: { x: number; y: number; }; - align?: boolean; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + blendMode?: BlendMode; + opacity?: number; + scale?: number; + pattern?: EffectPattern; + phase?: { x: number; y: number }; + align?: boolean; } export interface EffectSolidGradient { - name: string; - type: 'solid'; - smoothness?: number; - colorStops: ColorStop[]; - opacityStops: OpacityStop[]; + name: string; + type: "solid"; + smoothness?: number; + colorStops: ColorStop[]; + opacityStops: OpacityStop[]; } export interface EffectNoiseGradient { - name: string; - type: 'noise'; - roughness?: number; - colorModel?: 'rgb' | 'hsb' | 'lab'; - randomSeed?: number; - restrictColors?: boolean; - addTransparency?: boolean; - min: number[]; - max: number[]; + name: string; + type: "noise"; + roughness?: number; + colorModel?: "rgb" | "hsb" | "lab"; + randomSeed?: number; + restrictColors?: boolean; + addTransparency?: boolean; + min: number[]; + max: number[]; } export interface LayerEffectGradientOverlay { - present?: boolean; - showInDialog?: boolean; - enabled?: boolean; - blendMode?: string; - opacity?: number; - align?: boolean; - scale?: number; - dither?: boolean; - reverse?: boolean; - type?: GradientStyle; - offset?: { x: number; y: number; }; - gradient?: EffectSolidGradient | EffectNoiseGradient; - interpolationMethod?: InterpolationMethod; + present?: boolean; + showInDialog?: boolean; + enabled?: boolean; + blendMode?: string; + opacity?: number; + align?: boolean; + scale?: number; + dither?: boolean; + reverse?: boolean; + type?: GradientStyle; + offset?: { x: number; y: number }; + gradient?: EffectSolidGradient | EffectNoiseGradient; + interpolationMethod?: InterpolationMethod; } export interface LayerEffectsInfo { - disabled?: boolean; - scale?: number; - dropShadow?: LayerEffectShadow[]; - innerShadow?: LayerEffectShadow[]; - outerGlow?: LayerEffectsOuterGlow; - innerGlow?: LayerEffectInnerGlow; - bevel?: LayerEffectBevel; - solidFill?: LayerEffectSolidFill[]; - satin?: LayerEffectSatin; - stroke?: LayerEffectStroke[]; - gradientOverlay?: LayerEffectGradientOverlay[]; - patternOverlay?: LayerEffectPatternOverlay; // not supported yet because of `Patt` section not implemented -} - -export type PixelArray = Uint8ClampedArray | Uint8Array | Uint16Array | Float32Array; + disabled?: boolean; + scale?: number; + dropShadow?: LayerEffectShadow[]; + innerShadow?: LayerEffectShadow[]; + outerGlow?: LayerEffectsOuterGlow; + innerGlow?: LayerEffectInnerGlow; + bevel?: LayerEffectBevel; + solidFill?: LayerEffectSolidFill[]; + satin?: LayerEffectSatin; + stroke?: LayerEffectStroke[]; + gradientOverlay?: LayerEffectGradientOverlay[]; + patternOverlay?: LayerEffectPatternOverlay; // not supported yet because of `Patt` section not implemented +} + +export type PixelArray = + | Uint8ClampedArray + | Uint8Array + | Uint16Array + | Float32Array; export interface PixelData { - data: PixelArray; // type depends on document bit depth - width: number; - height: number; + data: PixelArray; // type depends on document bit depth + width: number; + height: number; } export interface LayerMaskData { - top?: number; - left?: number; - bottom?: number; - right?: number; - defaultColor?: number; - disabled?: boolean; - positionRelativeToLayer?: boolean; - fromVectorData?: boolean; // set to true if the mask is generated from vector data, false if it's a bitmap provided by user - userMaskDensity?: number; - userMaskFeather?: number; // px - vectorMaskDensity?: number; - vectorMaskFeather?: number; - canvas?: HTMLCanvasElement; - imageData?: PixelData; -} - -export type TextGridding = 'none' | 'round'; // TODO: other values (no idea where to set it up in Photoshop) -export type Orientation = 'horizontal' | 'vertical'; -export type AntiAlias = 'none' | 'sharp' | 'crisp' | 'strong' | 'smooth' | 'platform' | 'platformLCD'; + top?: number; + left?: number; + bottom?: number; + right?: number; + defaultColor?: number; + disabled?: boolean; + positionRelativeToLayer?: boolean; + fromVectorData?: boolean; // set to true if the mask is generated from vector data, false if it's a bitmap provided by user + userMaskDensity?: number; + userMaskFeather?: number; // px + vectorMaskDensity?: number; + vectorMaskFeather?: number; + canvas?: HTMLCanvasElement; + imageData?: PixelData; +} + +export type TextGridding = "none" | "round"; // TODO: other values (no idea where to set it up in Photoshop) +export type Orientation = "horizontal" | "vertical"; +export type AntiAlias = + | "none" + | "sharp" + | "crisp" + | "strong" + | "smooth" + | "platform" + | "platformLCD"; export type WarpStyle = - 'none' | 'arc' | 'arcLower' | 'arcUpper' | 'arch' | 'bulge' | 'shellLower' | 'shellUpper' | 'flag' | - 'wave' | 'fish' | 'rise' | 'fisheye' | 'inflate' | 'squeeze' | 'twist' | 'custom' | 'cylinder'; -export type BevelStyle = 'outer bevel' | 'inner bevel' | 'emboss' | 'pillow emboss' | 'stroke emboss'; -export type BevelTechnique = 'smooth' | 'chisel hard' | 'chisel soft'; -export type BevelDirection = 'up' | 'down'; -export type GlowTechnique = 'softer' | 'precise'; -export type GlowSource = 'edge' | 'center'; -export type GradientStyle = 'linear' | 'radial' | 'angle' | 'reflected' | 'diamond'; -export type Justification = 'left' | 'right' | 'center' | 'justify-left' | 'justify-right' | 'justify-center' | 'justify-all'; -export type LineCapType = 'butt' | 'round' | 'square'; -export type LineJoinType = 'miter' | 'round' | 'bevel'; -export type LineAlignment = 'inside' | 'center' | 'outside'; -export type InterpolationMethod = 'classic' | 'perceptual' | 'linear'; + | "none" + | "arc" + | "arcLower" + | "arcUpper" + | "arch" + | "bulge" + | "shellLower" + | "shellUpper" + | "flag" + | "wave" + | "fish" + | "rise" + | "fisheye" + | "inflate" + | "squeeze" + | "twist" + | "custom" + | "cylinder"; +export type BevelStyle = + | "outer bevel" + | "inner bevel" + | "emboss" + | "pillow emboss" + | "stroke emboss"; +export type BevelTechnique = "smooth" | "chisel hard" | "chisel soft"; +export type BevelDirection = "up" | "down"; +export type GlowTechnique = "softer" | "precise"; +export type GlowSource = "edge" | "center"; +export type GradientStyle = + | "linear" + | "radial" + | "angle" + | "reflected" + | "diamond"; +export type Justification = + | "left" + | "right" + | "center" + | "justify-left" + | "justify-right" + | "justify-center" + | "justify-all"; +export type LineCapType = "butt" | "round" | "square"; +export type LineJoinType = "miter" | "round" | "bevel"; +export type LineAlignment = "inside" | "center" | "outside"; +export type InterpolationMethod = "classic" | "perceptual" | "linear"; export interface Warp { - style?: WarpStyle; - value?: number; - values?: number[]; - perspective?: number; - perspectiveOther?: number; - rotate?: Orientation; - // for custom warps - bounds?: { top: UnitsValue; left: UnitsValue; bottom: UnitsValue; right: UnitsValue; }; - uOrder?: number; - vOrder?: number; - deformNumRows?: number; - deformNumCols?: number; - customEnvelopeWarp?: { - quiltSliceX?: number[]; - quiltSliceY?: number[]; - // 16 points from top left to bottom right, rows first, all points are relative to the first point - meshPoints: { x: number; y: number; }[]; - }; + style?: WarpStyle; + value?: number; + values?: number[]; + perspective?: number; + perspectiveOther?: number; + rotate?: Orientation; + // for custom warps + bounds?: { + top: UnitsValue; + left: UnitsValue; + bottom: UnitsValue; + right: UnitsValue; + }; + uOrder?: number; + vOrder?: number; + deformNumRows?: number; + deformNumCols?: number; + customEnvelopeWarp?: { + quiltSliceX?: number[]; + quiltSliceY?: number[]; + // 16 points from top left to bottom right, rows first, all points are relative to the first point + meshPoints: { x: number; y: number }[]; + }; } export interface Animations { - frames: { - id: number; - delay: number; - dispose?: 'auto' | 'none' | 'dispose'; - }[]; - animations: { - id: number; - frames: number[]; - repeats?: number; - activeFrame?: number; - }[]; + frames: { + id: number; + delay: number; + dispose?: "auto" | "none" | "dispose"; + }[]; + animations: { + id: number; + frames: number[]; + repeats?: number; + activeFrame?: number; + }[]; } export interface Font { - name: string; - script?: number; - type?: number; - synthetic?: number; + name: string; + script?: number; + type?: number; + synthetic?: number; } export interface ParagraphStyle { - justification?: Justification; - firstLineIndent?: number; - startIndent?: number; - endIndent?: number; - spaceBefore?: number; - spaceAfter?: number; - autoHyphenate?: boolean; - hyphenatedWordSize?: number; - preHyphen?: number; - postHyphen?: number; - consecutiveHyphens?: number; - zone?: number; - wordSpacing?: number[]; - letterSpacing?: number[]; - glyphSpacing?: number[]; - autoLeading?: number; - leadingType?: number; - hanging?: boolean; - burasagari?: boolean; - kinsokuOrder?: number; - everyLineComposer?: boolean; + justification?: Justification; + firstLineIndent?: number; + startIndent?: number; + endIndent?: number; + spaceBefore?: number; + spaceAfter?: number; + autoHyphenate?: boolean; + hyphenatedWordSize?: number; + preHyphen?: number; + postHyphen?: number; + consecutiveHyphens?: number; + zone?: number; + wordSpacing?: number[]; + letterSpacing?: number[]; + glyphSpacing?: number[]; + autoLeading?: number; + leadingType?: number; + hanging?: boolean; + burasagari?: boolean; + kinsokuOrder?: number; + everyLineComposer?: boolean; } export interface ParagraphStyleRun { - length: number; - style: ParagraphStyle; + length: number; + style: ParagraphStyle; } export interface TextStyle { - font?: Font; - fontSize?: number; - fauxBold?: boolean; - fauxItalic?: boolean; - autoLeading?: boolean; - leading?: number; - horizontalScale?: number; - verticalScale?: number; - tracking?: number; - autoKerning?: boolean; - kerning?: number; - baselineShift?: number; - fontCaps?: number; // 0 - none, 1 - small caps, 2 - all caps - fontBaseline?: number; // 0 - normal, 1 - superscript, 2 - subscript - underline?: boolean; - strikethrough?: boolean; - ligatures?: boolean; - dLigatures?: boolean; - baselineDirection?: number; - tsume?: number; - styleRunAlignment?: number; - language?: number; - noBreak?: boolean; - fillColor?: Color; - strokeColor?: Color; - fillFlag?: boolean; - strokeFlag?: boolean; - fillFirst?: boolean; - yUnderline?: number; - outlineWidth?: number; - characterDirection?: number; - hindiNumbers?: boolean; - kashida?: number; - diacriticPos?: number; + font?: Font; + fontSize?: number; + fauxBold?: boolean; + fauxItalic?: boolean; + autoLeading?: boolean; + leading?: number; + horizontalScale?: number; + verticalScale?: number; + tracking?: number; + autoKerning?: boolean; + kerning?: number; + baselineShift?: number; + fontCaps?: number; // 0 - none, 1 - small caps, 2 - all caps + fontBaseline?: number; // 0 - normal, 1 - superscript, 2 - subscript + underline?: boolean; + strikethrough?: boolean; + ligatures?: boolean; + dLigatures?: boolean; + baselineDirection?: number; + tsume?: number; + styleRunAlignment?: number; + language?: number; + noBreak?: boolean; + fillColor?: Color; + strokeColor?: Color; + fillFlag?: boolean; + strokeFlag?: boolean; + fillFirst?: boolean; + yUnderline?: number; + outlineWidth?: number; + characterDirection?: number; + hindiNumbers?: boolean; + kashida?: number; + diacriticPos?: number; } export interface TextStyleRun { - length: number; - style: TextStyle; + length: number; + style: TextStyle; } export interface TextGridInfo { - isOn?: boolean; - show?: boolean; - size?: number; - leading?: number; - color?: Color; - leadingFillColor?: Color; - alignLineHeightToGridFlags?: boolean; + isOn?: boolean; + show?: boolean; + size?: number; + leading?: number; + color?: Color; + leadingFillColor?: Color; + alignLineHeightToGridFlags?: boolean; } export interface UnitsBounds { - top: UnitsValue; - left: UnitsValue; - right: UnitsValue; - bottom: UnitsValue; + top: UnitsValue; + left: UnitsValue; + right: UnitsValue; + bottom: UnitsValue; } export interface LayerTextData { - text: string; - transform?: number[]; // 2d transform matrix [xx, xy, yx, yy, tx, ty] - antiAlias?: AntiAlias; - gridding?: TextGridding; - orientation?: Orientation; - index?: number; - warp?: Warp; - top?: number; - left?: number; - bottom?: number; - right?: number; - - gridInfo?: TextGridInfo; - useFractionalGlyphWidths?: boolean; - style?: TextStyle; // base style - styleRuns?: TextStyleRun[]; // spans of different style - paragraphStyle?: ParagraphStyle; // base paragraph style - paragraphStyleRuns?: ParagraphStyleRun[]; // style for each line - - superscriptSize?: number; - superscriptPosition?: number; - subscriptSize?: number; - subscriptPosition?: number; - smallCapSize?: number; - - shapeType?: 'point' | 'box'; - pointBase?: number[]; - boxBounds?: number[]; - bounds?: UnitsBounds; - boundingBox?: UnitsBounds; + text: string; + transform?: number[]; // 2d transform matrix [xx, xy, yx, yy, tx, ty] + antiAlias?: AntiAlias; + gridding?: TextGridding; + orientation?: Orientation; + index?: number; + warp?: Warp; + top?: number; + left?: number; + bottom?: number; + right?: number; + + gridInfo?: TextGridInfo; + useFractionalGlyphWidths?: boolean; + style?: TextStyle; // base style + styleRuns?: TextStyleRun[]; // spans of different style + paragraphStyle?: ParagraphStyle; // base paragraph style + paragraphStyleRuns?: ParagraphStyleRun[]; // style for each line + + superscriptSize?: number; + superscriptPosition?: number; + subscriptSize?: number; + subscriptPosition?: number; + smallCapSize?: number; + + shapeType?: "point" | "box"; + pointBase?: number[]; + boxBounds?: number[]; + bounds?: UnitsBounds; + boundingBox?: UnitsBounds; } export interface PatternInfo { - name: string; - id: string; - x: number; - y: number; - bounds: { x: number; y: number; w: number, h: number; }; - data: Uint8Array; + name: string; + id: string; + x: number; + y: number; + bounds: { x: number; y: number; w: number; h: number }; + data: Uint8Array; } export interface BezierKnot { - linked: boolean; - points: number[]; // x0, y0, x1, y1, x2, y2 + linked: boolean; + points: number[]; // x0, y0, x1, y1, x2, y2 } -export type BooleanOperation = 'exclude' | 'combine' | 'subtract' | 'intersect'; +export type BooleanOperation = "exclude" | "combine" | "subtract" | "intersect"; export interface BezierPath { - open: boolean; - operation: BooleanOperation; - knots: BezierKnot[]; - fillRule: 'even-odd' | 'non-zero'; + open: boolean; + operation: BooleanOperation; + knots: BezierKnot[]; + fillRule: "even-odd" | "non-zero"; } export interface ExtraGradientInfo { - style?: GradientStyle; - scale?: number; - angle?: number; - dither?: boolean; - reverse?: boolean; - align?: boolean; - offset?: { x: number; y: number; }; + style?: GradientStyle; + scale?: number; + angle?: number; + dither?: boolean; + reverse?: boolean; + align?: boolean; + offset?: { x: number; y: number }; } export interface ExtraPatternInfo { - linked?: boolean; - phase?: { x: number; y: number; }; -} - -export type VectorContent = { type: 'color'; color: Color; } | - (EffectSolidGradient & ExtraGradientInfo) | - (EffectNoiseGradient & ExtraGradientInfo) | - (EffectPattern & { type: 'pattern'; } & ExtraPatternInfo); - -export type RenderingIntent = 'perceptual' | 'saturation' | 'relative colorimetric' | 'absolute colorimetric'; - -export type Units = 'Pixels' | 'Points' | 'Picas' | 'Millimeters' | 'Centimeters' | 'Inches' | 'None' | 'Density'; + linked?: boolean; + phase?: { x: number; y: number }; +} + +export type VectorContent = + | { type: "color"; color: Color } + | (EffectSolidGradient & ExtraGradientInfo) + | (EffectNoiseGradient & ExtraGradientInfo) + | (EffectPattern & { type: "pattern" } & ExtraPatternInfo); + +export type RenderingIntent = + | "perceptual" + | "saturation" + | "relative colorimetric" + | "absolute colorimetric"; + +export type Units = + | "Pixels" + | "Points" + | "Picas" + | "Millimeters" + | "Centimeters" + | "Inches" + | "None" + | "Density"; export interface UnitsValue { - units: Units; - value: number; + units: Units; + value: number; } export interface BrightnessAdjustment { - type: 'brightness/contrast'; - brightness?: number; - contrast?: number; - meanValue?: number; - useLegacy?: boolean; - labColorOnly?: boolean; - auto?: boolean; + type: "brightness/contrast"; + brightness?: number; + contrast?: number; + meanValue?: number; + useLegacy?: boolean; + labColorOnly?: boolean; + auto?: boolean; } export interface LevelsAdjustmentChannel { - shadowInput: number; - highlightInput: number; - shadowOutput: number; - highlightOutput: number; - midtoneInput: number; + shadowInput: number; + highlightInput: number; + shadowOutput: number; + highlightOutput: number; + midtoneInput: number; } export interface PresetInfo { - presetKind?: number; - presetFileName?: string; + presetKind?: number; + presetFileName?: string; } export interface LevelsAdjustment extends PresetInfo { - type: 'levels'; - rgb?: LevelsAdjustmentChannel; - red?: LevelsAdjustmentChannel; - green?: LevelsAdjustmentChannel; - blue?: LevelsAdjustmentChannel; + type: "levels"; + rgb?: LevelsAdjustmentChannel; + red?: LevelsAdjustmentChannel; + green?: LevelsAdjustmentChannel; + blue?: LevelsAdjustmentChannel; } -export type CurvesAdjustmentChannel = { input: number; output: number; }[]; +export type CurvesAdjustmentChannel = { input: number; output: number }[]; export interface CurvesAdjustment extends PresetInfo { - type: 'curves'; - rgb?: CurvesAdjustmentChannel; - red?: CurvesAdjustmentChannel; - green?: CurvesAdjustmentChannel; - blue?: CurvesAdjustmentChannel; + type: "curves"; + rgb?: CurvesAdjustmentChannel; + red?: CurvesAdjustmentChannel; + green?: CurvesAdjustmentChannel; + blue?: CurvesAdjustmentChannel; } export interface ExposureAdjustment extends PresetInfo { - type: 'exposure'; - exposure?: number; - offset?: number; - gamma?: number; + type: "exposure"; + exposure?: number; + offset?: number; + gamma?: number; } export interface VibranceAdjustment { - type: 'vibrance'; - vibrance?: number; - saturation?: number; + type: "vibrance"; + vibrance?: number; + saturation?: number; } export interface HueSaturationAdjustmentChannel { - a: number; - b: number; - c: number; - d: number; - hue: number; - saturation: number; - lightness: number; + a: number; + b: number; + c: number; + d: number; + hue: number; + saturation: number; + lightness: number; } export interface HueSaturationAdjustment extends PresetInfo { - type: 'hue/saturation'; - master?: HueSaturationAdjustmentChannel; - reds?: HueSaturationAdjustmentChannel; - yellows?: HueSaturationAdjustmentChannel; - greens?: HueSaturationAdjustmentChannel; - cyans?: HueSaturationAdjustmentChannel; - blues?: HueSaturationAdjustmentChannel; - magentas?: HueSaturationAdjustmentChannel; + type: "hue/saturation"; + master?: HueSaturationAdjustmentChannel; + reds?: HueSaturationAdjustmentChannel; + yellows?: HueSaturationAdjustmentChannel; + greens?: HueSaturationAdjustmentChannel; + cyans?: HueSaturationAdjustmentChannel; + blues?: HueSaturationAdjustmentChannel; + magentas?: HueSaturationAdjustmentChannel; } export interface ColorBalanceValues { - cyanRed: number; - magentaGreen: number; - yellowBlue: number; + cyanRed: number; + magentaGreen: number; + yellowBlue: number; } export interface ColorBalanceAdjustment { - type: 'color balance'; - shadows?: ColorBalanceValues; - midtones?: ColorBalanceValues; - highlights?: ColorBalanceValues; - preserveLuminosity?: boolean; + type: "color balance"; + shadows?: ColorBalanceValues; + midtones?: ColorBalanceValues; + highlights?: ColorBalanceValues; + preserveLuminosity?: boolean; } export interface BlackAndWhiteAdjustment extends PresetInfo { - type: 'black & white'; - reds?: number; - yellows?: number; - greens?: number; - cyans?: number; - blues?: number; - magentas?: number; - useTint?: boolean; - tintColor?: Color; + type: "black & white"; + reds?: number; + yellows?: number; + greens?: number; + cyans?: number; + blues?: number; + magentas?: number; + useTint?: boolean; + tintColor?: Color; } export interface PhotoFilterAdjustment { - type: 'photo filter'; - color?: Color; - density?: number; - preserveLuminosity?: boolean; + type: "photo filter"; + color?: Color; + density?: number; + preserveLuminosity?: boolean; } export interface ChannelMixerChannel { - red: number; - green: number; - blue: number; - constant: number; + red: number; + green: number; + blue: number; + constant: number; } export interface ChannelMixerAdjustment extends PresetInfo { - type: 'channel mixer'; - monochrome?: boolean; - red?: ChannelMixerChannel; - green?: ChannelMixerChannel; - blue?: ChannelMixerChannel; - gray?: ChannelMixerChannel; + type: "channel mixer"; + monochrome?: boolean; + red?: ChannelMixerChannel; + green?: ChannelMixerChannel; + blue?: ChannelMixerChannel; + gray?: ChannelMixerChannel; } export interface ColorLookupAdjustment { - type: 'color lookup'; - lookupType?: '3dlut' | 'abstractProfile' | 'deviceLinkProfile'; - name?: string; - dither?: boolean; - profile?: Uint8Array; - lutFormat?: 'look' | 'cube' | '3dl'; - dataOrder?: 'rgb' | 'bgr'; - tableOrder?: 'rgb' | 'bgr'; - lut3DFileData?: Uint8Array; - lut3DFileName?: string; + type: "color lookup"; + lookupType?: "3dlut" | "abstractProfile" | "deviceLinkProfile"; + name?: string; + dither?: boolean; + profile?: Uint8Array; + lutFormat?: "look" | "cube" | "3dl"; + dataOrder?: "rgb" | "bgr"; + tableOrder?: "rgb" | "bgr"; + lut3DFileData?: Uint8Array; + lut3DFileName?: string; } export interface InvertAdjustment { - type: 'invert'; + type: "invert"; } export interface PosterizeAdjustment { - type: 'posterize'; - levels?: number; + type: "posterize"; + levels?: number; } export interface ThresholdAdjustment { - type: 'threshold'; - level?: number; + type: "threshold"; + level?: number; } export interface ColorStop { - color: Color; - location: number; - midpoint: number; + color: Color; + location: number; + midpoint: number; } export interface OpacityStop { - opacity: number; - location: number; - midpoint: number; + opacity: number; + location: number; + midpoint: number; } export interface GradientMapAdjustment { - type: 'gradient map'; - name?: string; - gradientType: 'solid' | 'noise'; - dither?: boolean; - reverse?: boolean; - // solid - smoothness?: number; - colorStops?: ColorStop[]; - opacityStops?: OpacityStop[]; - // noise - roughness?: number; - colorModel?: 'rgb' | 'hsb' | 'lab'; - randomSeed?: number; - restrictColors?: boolean; - addTransparency?: boolean; - min?: number[]; - max?: number[]; + type: "gradient map"; + name?: string; + gradientType: "solid" | "noise"; + dither?: boolean; + reverse?: boolean; + // solid + smoothness?: number; + colorStops?: ColorStop[]; + opacityStops?: OpacityStop[]; + // noise + roughness?: number; + colorModel?: "rgb" | "hsb" | "lab"; + randomSeed?: number; + restrictColors?: boolean; + addTransparency?: boolean; + min?: number[]; + max?: number[]; } export interface SelectiveColorAdjustment { - type: 'selective color'; - mode?: 'relative' | 'absolute'; - reds?: CMYK; - yellows?: CMYK; - greens?: CMYK; - cyans?: CMYK; - blues?: CMYK; - magentas?: CMYK; - whites?: CMYK; - neutrals?: CMYK; - blacks?: CMYK; + type: "selective color"; + mode?: "relative" | "absolute"; + reds?: CMYK; + yellows?: CMYK; + greens?: CMYK; + cyans?: CMYK; + blues?: CMYK; + magentas?: CMYK; + whites?: CMYK; + neutrals?: CMYK; + blacks?: CMYK; } export interface LinkedFile { - id: string; // must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4) - name: string; - type?: string; - creator?: string; - data?: Uint8Array; - time?: string; // for external files - descriptor?: { - compInfo: { compID: number; originalCompID: number; }; - }; - childDocumentID?: string; - assetModTime?: number; - assetLockedState?: number; -} - -type FilterVariant = { - type: 'average' | 'blur' | 'blur more'; -} | { - type: 'box blur'; - filter: { - radius: UnitsValue; - }; -} | { - type: 'gaussian blur'; - filter: { - radius: UnitsValue; - }; -} | { - type: 'motion blur'; - filter: { - angle: number; // in degrees - distance: UnitsValue; - }; -} | { - type: 'radial blur'; - filter: { - amount: number; - method: 'spin' | 'zoom'; - quality: 'draft' | 'good' | 'best'; - }; -} | { - type: 'shape blur'; - filter: { - radius: UnitsValue; - customShape: { name: string; id: string }; - }; -} | { - type: 'smart blur'; - filter: { - radius: number; - threshold: number; - quality: 'low' | 'medium' | 'high'; - mode: 'normal' | 'edge only' | 'overlay edge'; - }; -} | { - type: 'surface blur'; - filter: { - radius: UnitsValue; - threshold: number; - }; -} | { - type: 'displace'; - filter: { - horizontalScale: number; - verticalScale: number; - displacementMap: 'stretch to fit' | 'tile'; - undefinedAreas: 'wrap around' | 'repeat edge pixels'; - displacementFile: { - signature: string; - path: string; - }; - }; -} | { - type: 'pinch'; - filter: { - amount: number; - }; -} | { - type: 'polar coordinates'; - filter: { - conversion: 'rectangular to polar' | 'polar to rectangular'; - }; -} | { - type: 'ripple'; - filter: { - amount: number; - size: 'small' | 'medium' | 'large'; - }; -} | { - type: 'shear'; - filter: { - shearPoints: { x: number; y: number }[]; - shearStart: number; - shearEnd: number; - undefinedAreas: 'wrap around' | 'repeat edge pixels'; - }; -} | { - type: 'spherize'; - filter: { - amount: number; - mode: 'normal' | 'horizontal only' | 'vertical only'; - }; -} | { - type: 'twirl'; - filter: { - angle: number; // degrees - }; -} | { - type: 'wave'; - filter: { - numberOfGenerators: number; - type: 'sine' | 'triangle' | 'square'; - wavelength: { min: number; max: number }; - amplitude: { min: number; max: number }; - scale: { x: number; y: number }; - randomSeed: number; - undefinedAreas: 'wrap around' | 'repeat edge pixels'; - }; -} | { - type: 'zigzag'; - filter: { - amount: number; - ridges: number; - style: 'around center' | 'out from center' | 'pond ripples'; - }; -} | { - type: 'add noise'; - filter: { - amount: number; // 0..1 - distribution: 'uniform' | 'gaussian'; - monochromatic: boolean; - randomSeed: number; - }; -} | { - type: 'despeckle'; -} | { - type: 'dust and scratches'; - filter: { - radius: number; // pixels - threshold: number; // levels - }; -} | { - type: 'median'; - filter: { - radius: UnitsValue; - }; -} | { - type: 'reduce noise'; - filter: { - preset: string; - removeJpegArtifact: boolean; - reduceColorNoise: number; // 0..1 - sharpenDetails: number; // 0..1 - channelDenoise: { - channels: ('red' | 'green' | 'blue' | 'composite')[]; - amount: number; - preserveDetails?: number; // percent - }[]; - }; -} | { - type: 'color halftone'; - filter: { - radius: number; // pixels - angle1: number; // degrees - angle2: number; // degrees - angle3: number; // degrees - angle4: number; // degrees - }; -} | { - type: 'crystallize'; - filter: { - cellSize: number; - randomSeed: number; - }; -} | { - type: 'facet' | 'fragment'; -} | { - type: 'mezzotint'; - filter: { - type: 'fine dots' | 'medium dots' | 'grainy dots' | 'coarse dots' | 'short lines' | 'medium lines' | 'long lines' | 'short strokes' | 'medium strokes' | 'long strokes'; - randomSeed: number; - }; -} | { - type: 'mosaic'; - filter: { - cellSize: UnitsValue; - }; -} | { - type: 'pointillize'; - filter: { - cellSize: number; - randomSeed: number; - }; -} | { - type: 'clouds'; - filter: { - randomSeed: number; - }; -} | { - type: 'difference clouds'; - filter: { - randomSeed: number; - }; -} | { - type: 'fibers'; - filter: { - variance: number; - strength: number; - randomSeed: number; - }; -} | { - type: 'lens flare'; - filter: { - brightness: number; // percent - position: { x: number; y: number; }; - lensType: '50-300mm zoom' | '32mm prime' | '105mm prime' | 'movie prime'; - }; -} /*| { + id: string; // must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4) + name: string; + type?: string; + creator?: string; + data?: Uint8Array; + time?: string; // for external files + descriptor?: { + compInfo: { compID: number; originalCompID: number }; + }; + childDocumentID?: string; + assetModTime?: number; + assetLockedState?: number; +} + +type FilterVariant = + | { + type: "average" | "blur" | "blur more"; + } + | { + type: "box blur"; + filter: { + radius: UnitsValue; + }; + } + | { + type: "gaussian blur"; + filter: { + radius: UnitsValue; + }; + } + | { + type: "motion blur"; + filter: { + angle: number; // in degrees + distance: UnitsValue; + }; + } + | { + type: "radial blur"; + filter: { + amount: number; + method: "spin" | "zoom"; + quality: "draft" | "good" | "best"; + }; + } + | { + type: "shape blur"; + filter: { + radius: UnitsValue; + customShape: { name: string; id: string }; + }; + } + | { + type: "smart blur"; + filter: { + radius: number; + threshold: number; + quality: "low" | "medium" | "high"; + mode: "normal" | "edge only" | "overlay edge"; + }; + } + | { + type: "surface blur"; + filter: { + radius: UnitsValue; + threshold: number; + }; + } + | { + type: "displace"; + filter: { + horizontalScale: number; + verticalScale: number; + displacementMap: "stretch to fit" | "tile"; + undefinedAreas: "wrap around" | "repeat edge pixels"; + displacementFile: { + signature: string; + path: string; + }; + }; + } + | { + type: "pinch"; + filter: { + amount: number; + }; + } + | { + type: "polar coordinates"; + filter: { + conversion: "rectangular to polar" | "polar to rectangular"; + }; + } + | { + type: "ripple"; + filter: { + amount: number; + size: "small" | "medium" | "large"; + }; + } + | { + type: "shear"; + filter: { + shearPoints: { x: number; y: number }[]; + shearStart: number; + shearEnd: number; + undefinedAreas: "wrap around" | "repeat edge pixels"; + }; + } + | { + type: "spherize"; + filter: { + amount: number; + mode: "normal" | "horizontal only" | "vertical only"; + }; + } + | { + type: "twirl"; + filter: { + angle: number; // degrees + }; + } + | { + type: "wave"; + filter: { + numberOfGenerators: number; + type: "sine" | "triangle" | "square"; + wavelength: { min: number; max: number }; + amplitude: { min: number; max: number }; + scale: { x: number; y: number }; + randomSeed: number; + undefinedAreas: "wrap around" | "repeat edge pixels"; + }; + } + | { + type: "zigzag"; + filter: { + amount: number; + ridges: number; + style: "around center" | "out from center" | "pond ripples"; + }; + } + | { + type: "add noise"; + filter: { + amount: number; // 0..1 + distribution: "uniform" | "gaussian"; + monochromatic: boolean; + randomSeed: number; + }; + } + | { + type: "despeckle"; + } + | { + type: "dust and scratches"; + filter: { + radius: number; // pixels + threshold: number; // levels + }; + } + | { + type: "median"; + filter: { + radius: UnitsValue; + }; + } + | { + type: "reduce noise"; + filter: { + preset: string; + removeJpegArtifact: boolean; + reduceColorNoise: number; // 0..1 + sharpenDetails: number; // 0..1 + channelDenoise: { + channels: ("red" | "green" | "blue" | "composite")[]; + amount: number; + preserveDetails?: number; // percent + }[]; + }; + } + | { + type: "color halftone"; + filter: { + radius: number; // pixels + angle1: number; // degrees + angle2: number; // degrees + angle3: number; // degrees + angle4: number; // degrees + }; + } + | { + type: "crystallize"; + filter: { + cellSize: number; + randomSeed: number; + }; + } + | { + type: "facet" | "fragment"; + } + | { + type: "mezzotint"; + filter: { + type: + | "fine dots" + | "medium dots" + | "grainy dots" + | "coarse dots" + | "short lines" + | "medium lines" + | "long lines" + | "short strokes" + | "medium strokes" + | "long strokes"; + randomSeed: number; + }; + } + | { + type: "mosaic"; + filter: { + cellSize: UnitsValue; + }; + } + | { + type: "pointillize"; + filter: { + cellSize: number; + randomSeed: number; + }; + } + | { + type: "clouds"; + filter: { + randomSeed: number; + }; + } + | { + type: "difference clouds"; + filter: { + randomSeed: number; + }; + } + | { + type: "fibers"; + filter: { + variance: number; + strength: number; + randomSeed: number; + }; + } + | { + type: "lens flare"; + filter: { + brightness: number; // percent + position: { x: number; y: number }; + lensType: + | "50-300mm zoom" + | "32mm prime" + | "105mm prime" + | "movie prime"; + }; + } /*| { type: 'lighting effects'; filter: { lights: Light3D; @@ -908,168 +1040,192 @@ type FilterVariant = { width: number; height: number; }; -}*/ | { - type: 'sharpen' | 'sharpen edges' | 'sharpen more'; -} | { - type: 'smart sharpen'; - filter: { - amount: number; // 0..1 - radius: UnitsValue; - threshold: number; - angle: number; // degrees - moreAccurate: boolean; - blur: 'gaussian blur' | 'lens blur' | 'motion blur'; - preset: string; - shadow: { - fadeAmount: number; // 0..1 - tonalWidth: number; // 0..1 - radius: number; // px - }; - highlight: { - fadeAmount: number; // 0..1 - tonalWidth: number; // 0..1 - radius: number; // px - }; - }; -} | { - type: 'unsharp mask'; - filter: { - amount: number; // 0..1 - radius: UnitsValue; - threshold: number; // levels - }; -} | { - type: 'diffuse'; - filter: { - mode: 'normal' | 'darken only' | 'lighten only' | 'anisotropic'; - randomSeed: number; - }; -} | { - type: 'emboss'; - filter: { - angle: number; // degrees - height: number; // pixels - amount: number; // percent - }; -} | { - type: 'extrude'; - filter: { - type: 'blocks' | 'pyramids'; - size: number; // pixels - depth: number; - depthMode: 'random' | 'level-based'; - randomSeed: number; - solidFrontFaces: boolean; - maskIncompleteBlocks: boolean; - }; -} | { - type: 'find edges' | 'solarize'; -} | { - type: 'tiles'; - filter: { - numberOfTiles: number; - maximumOffset: number; // percent - fillEmptyAreaWith: 'background color' | 'foreground color' | 'inverse image' | 'unaltered image'; - randomSeed: number; - }; -} | { - type: 'trace contour'; - filter: { - level: number; - edge: 'lower' | 'upper'; - }; -} | { - type: 'wind'; - filter: { - method: 'wind' | 'blast' | 'stagger'; - direction: 'left' | 'right'; - }; -} | { - type: 'de-interlace'; - filter: { - eliminate: 'odd lines' | 'even lines'; - newFieldsBy: 'duplication' | 'interpolation'; - }; -} | { - type: 'ntsc colors'; -} | { - type: 'custom'; - filter: { - scale: number; - offset: number; - matrix: number[]; - }; -} | { - type: 'high pass' | 'maximum' | 'minimum'; - filter: { - radius: UnitsValue; - }; -} | { - type: 'offset'; - filter: { - horizontal: number; // pixels - vertical: number; // pixels - undefinedAreas: 'set to transparent' | 'repeat edge pixels' | 'wrap around'; - }; -} | { - type: 'puppet'; - filter: { - rigidType: boolean; - bounds: { x: number; y: number; }[]; - puppetShapeList: { - rigidType: boolean; - // VrsM: number; - // VrsN: number; - originalVertexArray: { x: number; y: number; }[]; - deformedVertexArray: { x: number; y: number; }[]; - indexArray: number[]; - pinOffsets: { x: number; y: number; }[]; - posFinalPins: { x: number; y: number; }[]; - pinVertexIndices: number[]; - selectedPin: number[]; - pinPosition: { x: number; y: number; }[]; - pinRotation: number[]; // in degrees - pinOverlay: boolean[]; - pinDepth: number[]; - meshQuality: number; - meshExpansion: number; - meshRigidity: number; - imageResolution: number; - meshBoundaryPath: { - pathComponents: { - shapeOperation: string; - paths: { - closed: boolean; - points: { - anchor: { x: UnitsValue; y: UnitsValue; }; - forward: { x: UnitsValue; y: UnitsValue; }; - backward: { x: UnitsValue; y: UnitsValue; }; - smooth: boolean; - }[]; - }[]; - }[]; - }; - }[]; - }; -} | { - type: 'oil paint plugin'; - filter: { - name: string; - gpu: boolean; - lighting: boolean; - // FPth ??? - parameters: { - name: string; - value: number; - }[]; - }; -} /*| { +}*/ + | { + type: "sharpen" | "sharpen edges" | "sharpen more"; + } + | { + type: "smart sharpen"; + filter: { + amount: number; // 0..1 + radius: UnitsValue; + threshold: number; + angle: number; // degrees + moreAccurate: boolean; + blur: "gaussian blur" | "lens blur" | "motion blur"; + preset: string; + shadow: { + fadeAmount: number; // 0..1 + tonalWidth: number; // 0..1 + radius: number; // px + }; + highlight: { + fadeAmount: number; // 0..1 + tonalWidth: number; // 0..1 + radius: number; // px + }; + }; + } + | { + type: "unsharp mask"; + filter: { + amount: number; // 0..1 + radius: UnitsValue; + threshold: number; // levels + }; + } + | { + type: "diffuse"; + filter: { + mode: "normal" | "darken only" | "lighten only" | "anisotropic"; + randomSeed: number; + }; + } + | { + type: "emboss"; + filter: { + angle: number; // degrees + height: number; // pixels + amount: number; // percent + }; + } + | { + type: "extrude"; + filter: { + type: "blocks" | "pyramids"; + size: number; // pixels + depth: number; + depthMode: "random" | "level-based"; + randomSeed: number; + solidFrontFaces: boolean; + maskIncompleteBlocks: boolean; + }; + } + | { + type: "find edges" | "solarize"; + } + | { + type: "tiles"; + filter: { + numberOfTiles: number; + maximumOffset: number; // percent + fillEmptyAreaWith: + | "background color" + | "foreground color" + | "inverse image" + | "unaltered image"; + randomSeed: number; + }; + } + | { + type: "trace contour"; + filter: { + level: number; + edge: "lower" | "upper"; + }; + } + | { + type: "wind"; + filter: { + method: "wind" | "blast" | "stagger"; + direction: "left" | "right"; + }; + } + | { + type: "de-interlace"; + filter: { + eliminate: "odd lines" | "even lines"; + newFieldsBy: "duplication" | "interpolation"; + }; + } + | { + type: "ntsc colors"; + } + | { + type: "custom"; + filter: { + scale: number; + offset: number; + matrix: number[]; + }; + } + | { + type: "high pass" | "maximum" | "minimum"; + filter: { + radius: UnitsValue; + }; + } + | { + type: "offset"; + filter: { + horizontal: number; // pixels + vertical: number; // pixels + undefinedAreas: + | "set to transparent" + | "repeat edge pixels" + | "wrap around"; + }; + } + | { + type: "puppet"; + filter: { + rigidType: boolean; + bounds: { x: number; y: number }[]; + puppetShapeList: { + rigidType: boolean; + // VrsM: number; + // VrsN: number; + originalVertexArray: { x: number; y: number }[]; + deformedVertexArray: { x: number; y: number }[]; + indexArray: number[]; + pinOffsets: { x: number; y: number }[]; + posFinalPins: { x: number; y: number }[]; + pinVertexIndices: number[]; + selectedPin: number[]; + pinPosition: { x: number; y: number }[]; + pinRotation: number[]; // in degrees + pinOverlay: boolean[]; + pinDepth: number[]; + meshQuality: number; + meshExpansion: number; + meshRigidity: number; + imageResolution: number; + meshBoundaryPath: { + pathComponents: { + shapeOperation: string; + paths: { + closed: boolean; + points: { + anchor: { x: UnitsValue; y: UnitsValue }; + forward: { x: UnitsValue; y: UnitsValue }; + backward: { x: UnitsValue; y: UnitsValue }; + smooth: boolean; + }[]; + }[]; + }[]; + }; + }[]; + }; + } + | { + type: "oil paint plugin"; + filter: { + name: string; + gpu: boolean; + lighting: boolean; + // FPth ??? + parameters: { + name: string; + value: number; + }[]; + }; + } /*| { type: 'lens correction'; filter: { profile: string; }; -}*//* | { +}*/ /* | { type: 'adaptive wide angle'; filter: { correction: 'fisheye' | 'perspective' | 'auto' | 'full spherical'; @@ -1079,7 +1235,7 @@ type FilterVariant = { imageX: number; imageY: number; }; -}*//* | { +}*/ /* | { type: 'filter gallery'; filter: { filter: 'colored pencil'; @@ -1087,29 +1243,32 @@ type FilterVariant = { strokePressure: number; paperBrightness: number; } | ...; -}*/ | { - type: 'hsb/hsl'; - filter: { - inputMode: 'rgb' | 'hsb' | 'hsl'; - rowOrder: 'rgb' | 'hsb' | 'hsl'; - }; -} | { - type: 'oil paint'; - filter: { - lightingOn: boolean; - stylization: number; - cleanliness: number; - brushScale: number; - microBrush: number; - lightDirection: number; // degrees - specularity: number; - }; -} | { - type: 'liquify'; - filter: { - liquifyMesh: Uint8Array; - }; -}; +}*/ + | { + type: "hsb/hsl"; + filter: { + inputMode: "rgb" | "hsb" | "hsl"; + rowOrder: "rgb" | "hsb" | "hsl"; + }; + } + | { + type: "oil paint"; + filter: { + lightingOn: boolean; + stylization: number; + cleanliness: number; + brushScale: number; + microBrush: number; + lightDirection: number; // degrees + specularity: number; + }; + } + | { + type: "liquify"; + filter: { + liquifyMesh: Uint8Array; + }; + }; /* export interface Position3D { @@ -1137,577 +1296,619 @@ export interface Light3D { */ export type Filter = FilterVariant & { - name: string; - opacity: number; - blendMode: BlendMode; - enabled: boolean; - hasOptions: boolean; - foregroundColor: Color; - backgroundColor: Color; -} + name: string; + opacity: number; + blendMode: BlendMode; + enabled: boolean; + hasOptions: boolean; + foregroundColor: Color; + backgroundColor: Color; +}; export interface PlacedLayerFilter { - enabled: boolean; - validAtPosition: boolean; - maskEnabled: boolean; - maskLinked: boolean; - maskExtendWithWhite: boolean; - list: Filter[]; + enabled: boolean; + validAtPosition: boolean; + maskEnabled: boolean; + maskLinked: boolean; + maskExtendWithWhite: boolean; + list: Filter[]; } -export type PlacedLayerType = 'unknown' | 'vector' | 'raster' | 'image stack'; +export type PlacedLayerType = "unknown" | "vector" | "raster" | "image stack"; export interface PlacedLayer { - id: string; // id of linked image file (psd.linkedFiles), must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4) - placed?: string; // unique id - type: PlacedLayerType; - pageNumber?: number; - totalPages?: number; - frameStep?: { numerator: number; denominator: number; }; - duration?: { numerator: number; denominator: number; }; - frameCount?: number; - transform: number[]; // x, y of 4 corners of the transform - nonAffineTransform?: number[]; // x, y of 4 corners of the transform - width?: number; // width of the linked image - height?: number; // height of the linked image - resolution?: UnitsValue; - // antialias ? - warp?: Warp; // warp coordinates are relative to the linked image size - crop?: number; - comp?: number; - compInfo?: { compID: number; originalCompID: number; }; - filter?: PlacedLayerFilter; -} - -export type AdjustmentLayer = BrightnessAdjustment | LevelsAdjustment | CurvesAdjustment | - ExposureAdjustment | VibranceAdjustment | HueSaturationAdjustment | ColorBalanceAdjustment | - BlackAndWhiteAdjustment | PhotoFilterAdjustment | ChannelMixerAdjustment | ColorLookupAdjustment | - InvertAdjustment | PosterizeAdjustment | ThresholdAdjustment | GradientMapAdjustment | - SelectiveColorAdjustment; - -export type LayerColor = 'none' | 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'violet' | 'gray'; + id: string; // id of linked image file (psd.linkedFiles), must be in a GUID format (example: 20953ddb-9391-11ec-b4f1-c15674f50bc4) + placed?: string; // unique id + type: PlacedLayerType; + pageNumber?: number; + totalPages?: number; + frameStep?: { numerator: number; denominator: number }; + duration?: { numerator: number; denominator: number }; + frameCount?: number; + transform: number[]; // x, y of 4 corners of the transform + nonAffineTransform?: number[]; // x, y of 4 corners of the transform + width?: number; // width of the linked image + height?: number; // height of the linked image + resolution?: UnitsValue; + // antialias ? + warp?: Warp; // warp coordinates are relative to the linked image size + crop?: number; + comp?: number; + compInfo?: { compID: number; originalCompID: number }; + filter?: PlacedLayerFilter; +} + +export type AdjustmentLayer = + | BrightnessAdjustment + | LevelsAdjustment + | CurvesAdjustment + | ExposureAdjustment + | VibranceAdjustment + | HueSaturationAdjustment + | ColorBalanceAdjustment + | BlackAndWhiteAdjustment + | PhotoFilterAdjustment + | ChannelMixerAdjustment + | ColorLookupAdjustment + | InvertAdjustment + | PosterizeAdjustment + | ThresholdAdjustment + | GradientMapAdjustment + | SelectiveColorAdjustment; + +export type LayerColor = + | "none" + | "red" + | "orange" + | "yellow" + | "green" + | "blue" + | "violet" + | "gray"; export interface KeyDescriptorItem { - keyShapeInvalidated?: boolean; - keyOriginType?: number; - keyOriginResolution?: number; - keyOriginRRectRadii?: { - topRight: UnitsValue; - topLeft: UnitsValue; - bottomLeft: UnitsValue; - bottomRight: UnitsValue; - }; - keyOriginShapeBoundingBox?: { - top: UnitsValue; - left: UnitsValue; - bottom: UnitsValue; - right: UnitsValue; - }; - keyOriginBoxCorners?: { x: number; y: number; }[]; - transform?: number[]; // 2d transform matrix [xx, xy, yx, yy, tx, ty] + keyShapeInvalidated?: boolean; + keyOriginType?: number; + keyOriginResolution?: number; + keyOriginRRectRadii?: { + topRight: UnitsValue; + topLeft: UnitsValue; + bottomLeft: UnitsValue; + bottomRight: UnitsValue; + }; + keyOriginShapeBoundingBox?: { + top: UnitsValue; + left: UnitsValue; + bottom: UnitsValue; + right: UnitsValue; + }; + keyOriginBoxCorners?: { x: number; y: number }[]; + transform?: number[]; // 2d transform matrix [xx, xy, yx, yy, tx, ty] } export interface LayerVectorMask { - invert?: boolean; - notLink?: boolean; - disable?: boolean; - fillStartsWithAllPixels?: boolean; - clipboard?: { - top: number; - left: number; - bottom: number; - right: number; - resolution: number; - }; - paths: BezierPath[]; + invert?: boolean; + notLink?: boolean; + disable?: boolean; + fillStartsWithAllPixels?: boolean; + clipboard?: { + top: number; + left: number; + bottom: number; + right: number; + resolution: number; + }; + paths: BezierPath[]; } export interface AnimationFrame { - frames: number[]; // IDs of frames that this modifiers applies to - enable?: boolean; - offset?: { x: number; y: number; }; - referencePoint?: { x: number; y: number; }; - opacity?: number; - effects?: LayerEffectsInfo; + frames: number[]; // IDs of frames that this modifiers applies to + enable?: boolean; + offset?: { x: number; y: number }; + referencePoint?: { x: number; y: number }; + opacity?: number; + effects?: LayerEffectsInfo; } export interface Fraction { - numerator: number; - denominator: number; + numerator: number; + denominator: number; } -export type TimelineKeyInterpolation = 'linear' | 'hold'; +export type TimelineKeyInterpolation = "linear" | "hold"; export type TimelineKey = { - interpolation: TimelineKeyInterpolation; - time: Fraction; - selected?: boolean; -} & ({ - type: 'opacity'; - value: number; -} | { - type: 'position'; - x: number; - y: number; -} | { - type: 'transform'; - scale: { x: number; y: number; }; - skew: { x: number; y: number; }; - rotation: number; - translation: { x: number; y: number; }; -} | { - type: 'style'; - style?: LayerEffectsInfo; -} | { - type: 'globalLighting'; - globalAngle: number; - globalAltitude: number; -}); - -export type TimelineTrackType = 'opacity' | 'style' | 'sheetTransform' | 'sheetPosition' | 'globalLighting'; + interpolation: TimelineKeyInterpolation; + time: Fraction; + selected?: boolean; +} & ( + | { + type: "opacity"; + value: number; + } + | { + type: "position"; + x: number; + y: number; + } + | { + type: "transform"; + scale: { x: number; y: number }; + skew: { x: number; y: number }; + rotation: number; + translation: { x: number; y: number }; + } + | { + type: "style"; + style?: LayerEffectsInfo; + } + | { + type: "globalLighting"; + globalAngle: number; + globalAltitude: number; + } +); + +export type TimelineTrackType = + | "opacity" + | "style" + | "sheetTransform" + | "sheetPosition" + | "globalLighting"; export interface TimelineTrack { - type: TimelineTrackType; - enabled?: boolean; - effectParams?: { - keys: TimelineKey[]; - fillCanvas: boolean; - zoomOrigin: number; - }; - keys: TimelineKey[]; + type: TimelineTrackType; + enabled?: boolean; + effectParams?: { + keys: TimelineKey[]; + fillCanvas: boolean; + zoomOrigin: number; + }; + keys: TimelineKey[]; } export interface Timeline { - start: Fraction; - duration: Fraction; - inTime: Fraction; - outTime: Fraction; - autoScope: boolean; - audioLevel: number; - tracks?: TimelineTrack[]; + start: Fraction; + duration: Fraction; + inTime: Fraction; + outTime: Fraction; + autoScope: boolean; + audioLevel: number; + tracks?: TimelineTrack[]; } export interface LayerAdditionalInfo { - name?: string; // layer name - nameSource?: string; // layer name source - id?: number; // layer id - version?: number; // layer version - mask?: LayerMaskData; - blendClippendElements?: boolean; // has to be set to `true` when using `color burn` blend mode (otherwise `transparencyShapesLayer` is set incorrectly) - blendInteriorElements?: boolean; - knockout?: boolean; - layerMaskAsGlobalMask?: boolean; - protected?: { - transparency?: boolean; - composite?: boolean; - position?: boolean; - artboards?: boolean; - }; - layerColor?: LayerColor; - referencePoint?: { - x: number; - y: number; - }; - sectionDivider?: { - type: SectionDividerType; - key?: string; - subType?: number; // 0 = normal, 1 = scene group, affects the animation timeline. - }; - filterMask?: { - colorSpace: Color; - opacity: number; - }; - effects?: LayerEffectsInfo; - text?: LayerTextData; - patterns?: PatternInfo[]; // not supported yet - vectorFill?: VectorContent; - vectorStroke?: { - strokeEnabled?: boolean; - fillEnabled?: boolean; - lineWidth?: UnitsValue; - lineDashOffset?: UnitsValue; - miterLimit?: number; - lineCapType?: LineCapType; - lineJoinType?: LineJoinType; - lineAlignment?: LineAlignment; - scaleLock?: boolean; - strokeAdjust?: boolean; - lineDashSet?: UnitsValue[]; - blendMode?: BlendMode; - opacity?: number; - content?: VectorContent; - resolution?: number; - }; - vectorMask?: LayerVectorMask; - usingAlignedRendering?: boolean; - timestamp?: number; // seconds - pathList?: { - // TODO: ... - }[]; - adjustment?: AdjustmentLayer; - placedLayer?: PlacedLayer; - vectorOrigination?: { - keyDescriptorList: KeyDescriptorItem[]; - }; - compositorUsed?: { - description: string; - reason: string; - engine: string; - enableCompCore?: string; - enableCompCoreGPU?: string; - compCoreSupport?: string; - compCoreGPUSupport?: string; - }; - artboard?: { - rect: { top: number; left: number; bottom: number; right: number; }; - guideIndices?: any[]; - presetName?: string; - color?: Color; - backgroundType?: number; - }; - fillOpacity?: number; - transparencyShapesLayer?: boolean; - channelBlendingRestrictions?: number[]; - animationFrames?: AnimationFrame[]; - animationFrameFlags?: { - propagateFrameOne?: boolean; - unifyLayerPosition?: boolean; - unifyLayerStyle?: boolean; - unifyLayerVisibility?: boolean; - }; - timeline?: Timeline; - filterEffectsMasks?: { - id: string; - top: number; - left: number; - bottom: number; - right: number; - depth: number; - channels: ({ - compressionMode: number; - data: Uint8Array; - } | undefined)[]; - extra?: { - compressionMode: number; - data: Uint8Array; - }; - }[]; - comps?: { - originalEffectsReferencePoint?: { x: number; y: number; }; - settings: { - enabled?: boolean; - compList: number[]; - offset?: { x: number; y: number; }; - effectsReferencePoint?: { x: number; y: number; }; - }[]; - }; - userMask?: { - colorSpace: Color; - opacity: number; - }; - - // Base64 encoded raw EngineData, currently just kept in original state to support - // loading and modifying PSD file without breaking text layers. - engineData?: string; + name?: string; // layer name + nameSource?: string; // layer name source + id?: number; // layer id + version?: number; // layer version + mask?: LayerMaskData; + blendClippendElements?: boolean; // has to be set to `true` when using `color burn` blend mode (otherwise `transparencyShapesLayer` is set incorrectly) + blendInteriorElements?: boolean; + knockout?: boolean; + layerMaskAsGlobalMask?: boolean; + protected?: { + transparency?: boolean; + composite?: boolean; + position?: boolean; + artboards?: boolean; + }; + layerColor?: LayerColor; + referencePoint?: { + x: number; + y: number; + }; + sectionDivider?: { + type: SectionDividerType; + key?: string; + subType?: number; // 0 = normal, 1 = scene group, affects the animation timeline. + }; + filterMask?: { + colorSpace: Color; + opacity: number; + }; + effects?: LayerEffectsInfo; + text?: LayerTextData; + patterns?: PatternInfo[]; // not supported yet + vectorFill?: VectorContent; + vectorStroke?: { + strokeEnabled?: boolean; + fillEnabled?: boolean; + lineWidth?: UnitsValue; + lineDashOffset?: UnitsValue; + miterLimit?: number; + lineCapType?: LineCapType; + lineJoinType?: LineJoinType; + lineAlignment?: LineAlignment; + scaleLock?: boolean; + strokeAdjust?: boolean; + lineDashSet?: UnitsValue[]; + blendMode?: BlendMode; + opacity?: number; + content?: VectorContent; + resolution?: number; + }; + vectorMask?: LayerVectorMask; + usingAlignedRendering?: boolean; + timestamp?: number; // seconds + pathList?: { + // TODO: ... + }[]; + adjustment?: AdjustmentLayer; + placedLayer?: PlacedLayer; + vectorOrigination?: { + keyDescriptorList: KeyDescriptorItem[]; + }; + compositorUsed?: { + description: string; + reason: string; + engine: string; + enableCompCore?: string; + enableCompCoreGPU?: string; + compCoreSupport?: string; + compCoreGPUSupport?: string; + }; + artboard?: { + rect: { top: number; left: number; bottom: number; right: number }; + guideIndices?: any[]; + presetName?: string; + color?: Color; + backgroundType?: number; + }; + fillOpacity?: number; + transparencyShapesLayer?: boolean; + channelBlendingRestrictions?: number[]; + animationFrames?: AnimationFrame[]; + animationFrameFlags?: { + propagateFrameOne?: boolean; + unifyLayerPosition?: boolean; + unifyLayerStyle?: boolean; + unifyLayerVisibility?: boolean; + }; + timeline?: Timeline; + filterEffectsMasks?: { + id: string; + top: number; + left: number; + bottom: number; + right: number; + depth: number; + channels: ( + | { + compressionMode: number; + data: Uint8Array; + } + | undefined + )[]; + extra?: { + compressionMode: number; + data: Uint8Array; + }; + }[]; + comps?: { + originalEffectsReferencePoint?: { x: number; y: number }; + settings: { + enabled?: boolean; + compList: number[]; + offset?: { x: number; y: number }; + effectsReferencePoint?: { x: number; y: number }; + }[]; + }; + userMask?: { + colorSpace: Color; + opacity: number; + }; + + // Base64 encoded raw EngineData, currently just kept in original state to support + // loading and modifying PSD file without breaking text layers. + engineData?: string; } export enum LayerCompCapturedInfo { - None = 0, - Visibility = 1, - Position = 2, - Appearance = 4, + None = 0, + Visibility = 1, + Position = 2, + Appearance = 4, } export interface ImageResources { - layerState?: number; - layersGroup?: number[]; - layerSelectionIds?: number[]; - layerGroupsEnabledId?: number[]; - versionInfo?: { - hasRealMergedData: boolean; - writerName: string; - readerName: string; - fileVersion: number; - }; - alphaIdentifiers?: number[]; - alphaChannelNames?: string[]; - globalAngle?: number; - globalAltitude?: number; - pixelAspectRatio?: { - aspect: number; - }; - urlsList?: { - id: number; - ref: 'slice'; - url: string; - }[]; - gridAndGuidesInformation?: { - grid?: { - horizontal: number; - vertical: number; - }, - guides?: { - location: number; - direction: 'horizontal' | 'vertical'; - }[]; - }; - resolutionInfo?: { - horizontalResolution: number; - horizontalResolutionUnit: 'PPI' | 'PPCM'; - widthUnit: 'Inches' | 'Centimeters' | 'Points' | 'Picas' | 'Columns'; - verticalResolution: number; - verticalResolutionUnit: 'PPI' | 'PPCM'; - heightUnit: 'Inches' | 'Centimeters' | 'Points' | 'Picas' | 'Columns'; - }; - thumbnail?: HTMLCanvasElement; - thumbnailRaw?: { width: number; height: number; data: Uint8Array; }; - captionDigest?: string; - xmpMetadata?: string; - printScale?: { - style?: 'centered' | 'size to fit' | 'user defined'; - x?: number; - y?: number; - scale?: number; - }; - printInformation?: { - printerManagesColors?: boolean; - printerName?: string; - printerProfile?: string; - printSixteenBit?: boolean; - renderingIntent?: RenderingIntent; - hardProof?: boolean; - blackPointCompensation?: boolean; - proofSetup?: { - builtin: string; - } | { - profile: string; - renderingIntent?: RenderingIntent; - blackPointCompensation?: boolean; - paperWhite?: boolean; - }; - }; - backgroundColor?: Color; - idsSeedNumber?: number; - printFlags?: { - labels?: boolean; - cropMarks?: boolean; - colorBars?: boolean; - registrationMarks?: boolean; - negative?: boolean; - flip?: boolean; - interpolate?: boolean; - caption?: boolean; - printFlags?: boolean; - }; - iccUntaggedProfile?: boolean; - pathSelectionState?: string[]; - imageReadyVariables?: string; - imageReadyDataSets?: string; - animations?: Animations; - onionSkins?: { - enabled: boolean; - framesBefore: number; - framesAfter: number; - frameSpacing: number; - minOpacity: number; - maxOpacity: number; - blendMode: BlendMode; - }; - timelineInformation?: { - enabled: boolean; - frameStep: Fraction; - frameRate: number; - time: Fraction; - duration: Fraction; - workInTime: Fraction; - workOutTime: Fraction; - repeats: number; - hasMotion: boolean; - globalTracks: TimelineTrack[]; - audioClipGroups?: { - id: string; - muted: boolean; - audioClips: { - id: string; - start: Fraction; - duration: Fraction; - inTime: Fraction; - outTime: Fraction; - muted: boolean; - audioLevel: number; - frameReader: { - type: number; - mediaDescriptor: string; - link: { - name: string; - fullPath: string; - relativePath: string; - }; - }; - }[]; - }[]; - }; - sheetDisclosure?: { - sheetTimelineOptions?: { - sheetID: number; - sheetDisclosed: boolean; - lightsDisclosed: boolean; - meshesDisclosed: boolean; - materialsDisclosed: boolean; - }[]; - }; - countInformation?: { - color: RGB; - name: string; - size: number; - fontSize: number; - visible: boolean; - points: { x: number; y: number }[]; - }[]; - slices?: { - bounds: { left: number; top: number; right: number; bottom: number }; - groupName: string; - slices: { - id: number; - groupId: number; - origin: 'userGenerated' | 'autoGenerated' | 'layer'; - associatedLayerId: number; - name?: string; - type: 'image' | 'noImage'; - bounds: { left: number; top: number; right: number; bottom: number }; - url: string; - target: string; - message: string; - altTag: string; - cellTextIsHTML: boolean; - cellText: string; - horizontalAlignment: 'default'; - verticalAlignment: 'default'; - backgroundColorType: 'none' | 'matte' | 'color'; - backgroundColor: RGBA; - topOutset?: number; - leftOutset?: number; - bottomOutset?: number; - rightOutset?: number; - }[]; - }[]; - layerComps?: { - list: { - id: number; - name: string; - comment?: string; - capturedInfo: LayerCompCapturedInfo; - }[]; - lastApplied?: number; - }; + layerState?: number; + layersGroup?: number[]; + layerSelectionIds?: number[]; + layerGroupsEnabledId?: number[]; + versionInfo?: { + hasRealMergedData: boolean; + writerName: string; + readerName: string; + fileVersion: number; + }; + alphaIdentifiers?: number[]; + alphaChannelNames?: string[]; + globalAngle?: number; + globalAltitude?: number; + pixelAspectRatio?: { + aspect: number; + }; + urlsList?: { + id: number; + ref: "slice"; + url: string; + }[]; + gridAndGuidesInformation?: { + grid?: { + horizontal: number; + vertical: number; + }; + guides?: { + location: number; + direction: "horizontal" | "vertical"; + }[]; + }; + resolutionInfo?: { + horizontalResolution: number; + horizontalResolutionUnit: "PPI" | "PPCM"; + widthUnit: "Inches" | "Centimeters" | "Points" | "Picas" | "Columns"; + verticalResolution: number; + verticalResolutionUnit: "PPI" | "PPCM"; + heightUnit: "Inches" | "Centimeters" | "Points" | "Picas" | "Columns"; + }; + thumbnail?: HTMLCanvasElement; + thumbnailRaw?: { width: number; height: number; data: Uint8Array }; + captionDigest?: string; + xmpMetadata?: string; + printScale?: { + style?: "centered" | "size to fit" | "user defined"; + x?: number; + y?: number; + scale?: number; + }; + printInformation?: { + printerManagesColors?: boolean; + printerName?: string; + printerProfile?: string; + printSixteenBit?: boolean; + renderingIntent?: RenderingIntent; + hardProof?: boolean; + blackPointCompensation?: boolean; + proofSetup?: + | { + builtin: string; + } + | { + profile: string; + renderingIntent?: RenderingIntent; + blackPointCompensation?: boolean; + paperWhite?: boolean; + }; + }; + backgroundColor?: Color; + idsSeedNumber?: number; + printFlags?: { + labels?: boolean; + cropMarks?: boolean; + colorBars?: boolean; + registrationMarks?: boolean; + negative?: boolean; + flip?: boolean; + interpolate?: boolean; + caption?: boolean; + printFlags?: boolean; + }; + iccUntaggedProfile?: boolean; + pathSelectionState?: string[]; + imageReadyVariables?: string; + imageReadyDataSets?: string; + animations?: Animations; + onionSkins?: { + enabled: boolean; + framesBefore: number; + framesAfter: number; + frameSpacing: number; + minOpacity: number; + maxOpacity: number; + blendMode: BlendMode; + }; + timelineInformation?: { + enabled: boolean; + frameStep: Fraction; + frameRate: number; + time: Fraction; + duration: Fraction; + workInTime: Fraction; + workOutTime: Fraction; + repeats: number; + hasMotion: boolean; + globalTracks: TimelineTrack[]; + audioClipGroups?: { + id: string; + muted: boolean; + audioClips: { + id: string; + start: Fraction; + duration: Fraction; + inTime: Fraction; + outTime: Fraction; + muted: boolean; + audioLevel: number; + frameReader: { + type: number; + mediaDescriptor: string; + link: { + name: string; + fullPath: string; + relativePath: string; + }; + }; + }[]; + }[]; + }; + sheetDisclosure?: { + sheetTimelineOptions?: { + sheetID: number; + sheetDisclosed: boolean; + lightsDisclosed: boolean; + meshesDisclosed: boolean; + materialsDisclosed: boolean; + }[]; + }; + countInformation?: { + color: RGB; + name: string; + size: number; + fontSize: number; + visible: boolean; + points: { x: number; y: number }[]; + }[]; + slices?: { + bounds: { left: number; top: number; right: number; bottom: number }; + groupName: string; + slices: { + id: number; + groupId: number; + origin: "userGenerated" | "autoGenerated" | "layer"; + associatedLayerId: number; + name?: string; + type: "image" | "noImage"; + bounds: { left: number; top: number; right: number; bottom: number }; + url: string; + target: string; + message: string; + altTag: string; + cellTextIsHTML: boolean; + cellText: string; + horizontalAlignment: "default"; + verticalAlignment: "default"; + backgroundColorType: "none" | "matte" | "color"; + backgroundColor: RGBA; + topOutset?: number; + leftOutset?: number; + bottomOutset?: number; + rightOutset?: number; + }[]; + }[]; + layerComps?: { + list: { + id: number; + name: string; + comment?: string; + capturedInfo: LayerCompCapturedInfo; + }[]; + lastApplied?: number; + }; } export interface GlobalLayerMaskInfo { - overlayColorSpace: number; - colorSpace1: number; - colorSpace2: number; - colorSpace3: number; - colorSpace4: number; - opacity: number; - kind: number; + overlayColorSpace: number; + colorSpace1: number; + colorSpace2: number; + colorSpace3: number; + colorSpace4: number; + opacity: number; + kind: number; } export interface Annotation { - type: 'text' | 'sound'; - open: boolean; - iconLocation: { left: number; top: number; right: number; bottom: number }; - popupLocation: { left: number; top: number; right: number; bottom: number }; - color: Color; - author: string; - name: string; - date: string; - data: string | Uint8Array; + type: "text" | "sound"; + open: boolean; + iconLocation: { left: number; top: number; right: number; bottom: number }; + popupLocation: { left: number; top: number; right: number; bottom: number }; + color: Color; + author: string; + name: string; + date: string; + data: string | Uint8Array; } export interface Layer extends LayerAdditionalInfo { - top?: number; - left?: number; - bottom?: number; - right?: number; - blendMode?: BlendMode; - opacity?: number; - transparencyProtected?: boolean; - effectsOpen?: boolean; // effects/filters panel is expanded - hidden?: boolean; - clipping?: boolean; - canvas?: HTMLCanvasElement; - imageData?: PixelData; - children?: Layer[]; - /** Applies only for layer groups. */ - opened?: boolean; + top?: number; + left?: number; + bottom?: number; + right?: number; + blendMode?: BlendMode; + opacity?: number; + transparencyProtected?: boolean; + effectsOpen?: boolean; // effects/filters panel is expanded + hidden?: boolean; + clipping?: boolean; + canvas?: HTMLCanvasElement; + imageData?: PixelData; + children?: Layer[]; + /** Applies only for layer groups. */ + opened?: boolean; } export interface Psd extends LayerAdditionalInfo { - width: number; - height: number; - channels?: number; - bitsPerChannel?: number; - colorMode?: ColorMode; - children?: Layer[]; - canvas?: HTMLCanvasElement; - imageData?: PixelData; - imageResources?: ImageResources; - linkedFiles?: LinkedFile[]; // used in smart objects - artboards?: { - count: number; // number of artboards in the document - autoExpandOffset?: { horizontal: number; vertical: number; }; - origin?: { horizontal: number; vertical: number; }; - autoExpandEnabled?: boolean; - autoNestEnabled?: boolean; - autoPositionEnabled?: boolean; - shrinkwrapOnSaveEnabled?: boolean; - docDefaultNewArtboardBackgroundColor?: Color; - docDefaultNewArtboardBackgroundType?: number; - }; - globalLayerMaskInfo?: GlobalLayerMaskInfo; - annotations?: Annotation[]; -} + width: number; + height: number; + channels?: number; + bitsPerChannel?: number; + colorMode?: ColorMode; + children?: Layer[]; + canvas?: HTMLCanvasElement; + imageData?: PixelData; + imageResources?: ImageResources; + linkedFiles?: LinkedFile[]; // used in smart objects + artboards?: { + count: number; // number of artboards in the document + autoExpandOffset?: { horizontal: number; vertical: number }; + origin?: { horizontal: number; vertical: number }; + autoExpandEnabled?: boolean; + autoNestEnabled?: boolean; + autoPositionEnabled?: boolean; + shrinkwrapOnSaveEnabled?: boolean; + docDefaultNewArtboardBackgroundColor?: Color; + docDefaultNewArtboardBackgroundType?: number; + }; + globalLayerMaskInfo?: GlobalLayerMaskInfo; + annotations?: Annotation[]; +} + +export type PostImageDataHandler = ( + imageData: PixelData, + id?: number +) => Promise; export interface ReadOptions { - /** Does not load layer image data. */ - skipLayerImageData?: boolean; - /** Does not load composite image data. */ - skipCompositeImageData?: boolean; - /** Does not load thumbnail. */ - skipThumbnail?: boolean; - /** Does not load linked files (used in smart-objects). */ - skipLinkedFilesData?: boolean; - /** Throws exception if features are missing. */ - throwForMissingFeatures?: boolean; - /** Logs if features are missing. */ - logMissingFeatures?: boolean; - /** Keep image data as byte array instead of canvas. - * (image data will appear in `imageData` fields instead of `canvas` fields) - * This avoids issues with canvas premultiplied alpha corrupting image data. */ - useImageData?: boolean; - /** Loads thumbnail raw data instead of decoding it's content into canvas. - * `thumnailRaw` field is used instead. */ - useRawThumbnail?: boolean; - /** Usend only for development. */ - logDevFeatures?: boolean; + /** Does not load layer image data. */ + skipLayerImageData?: boolean; + /** Does not load composite image data. */ + skipCompositeImageData?: boolean; + /** Does not load thumbnail. */ + skipThumbnail?: boolean; + /** Does not load linked files (used in smart-objects). */ + skipLinkedFilesData?: boolean; + /** Throws exception if features are missing. */ + throwForMissingFeatures?: boolean; + /** Logs if features are missing. */ + logMissingFeatures?: boolean; + /** Keep image data as byte array instead of canvas. + * (image data will appear in `imageData` fields instead of `canvas` fields) + * This avoids issues with canvas premultiplied alpha corrupting image data. */ + useImageData?: boolean; + useCanvasData?: boolean; + /** Loads thumbnail raw data instead of decoding it's content into canvas. + * `thumnailRaw` field is used instead. */ + useRawThumbnail?: boolean; + /** Usend only for development. */ + logDevFeatures?: boolean; } export interface WriteOptions { - /** Automatically generates thumbnail from composite image. */ - generateThumbnail?: boolean; - /** Trims transparent pixels from layer image data. */ - trimImageData?: boolean; - /** Invalidates text layer data, forcing Photoshop to redraw them on load. - * Use this option if you're updating loaded text layer properties. */ - invalidateTextLayers?: boolean; - /** Logs if features are missing. */ - logMissingFeatures?: boolean; - /** Forces bottom layer to be treated as layer and not background even when it's missing any transparency - * (by default Photoshop treats bottom layer as background it it doesn't have any transparent pixels). */ - noBackground?: boolean; - /** Saves document as PSB (Large Document Format) file. */ - psb?: boolean; - /** Uses zip compression when writing PSD file, will result in smaller file size but may be incompatible - * with some software. It may also be significantly slower. */ - compress?: boolean; + /** Automatically generates thumbnail from composite image. */ + generateThumbnail?: boolean; + /** Trims transparent pixels from layer image data. */ + trimImageData?: boolean; + /** Invalidates text layer data, forcing Photoshop to redraw them on load. + * Use this option if you're updating loaded text layer properties. */ + invalidateTextLayers?: boolean; + /** Logs if features are missing. */ + logMissingFeatures?: boolean; + /** Forces bottom layer to be treated as layer and not background even when it's missing any transparency + * (by default Photoshop treats bottom layer as background it it doesn't have any transparent pixels). */ + noBackground?: boolean; + /** Saves document as PSB (Large Document Format) file. */ + psb?: boolean; + /** Uses zip compression when writing PSD file, will result in smaller file size but may be incompatible + * with some software. It may also be significantly slower. */ + compress?: boolean; } diff --git a/src/psdReader.ts b/src/psdReader.ts index 3478744..efb00cb 100644 --- a/src/psdReader.ts +++ b/src/psdReader.ts @@ -1,471 +1,595 @@ -import { inflate as inflateSync } from 'pako'; -import { Psd, Layer, ColorMode, SectionDividerType, LayerAdditionalInfo, ReadOptions, LayerMaskData, Color, PatternInfo, GlobalLayerMaskInfo, RGB, PixelData, PixelArray } from './psd'; -import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, ChannelID, Compression, LayerMaskFlags, MaskParams, ColorSpace, RAW_IMAGE_DATA, largeAdditionalInfoKeys, imageDataToCanvas } from './helpers'; -import { infoHandlersMap } from './additionalInfo'; -import { resourceHandlersMap } from './imageResources'; +import { inflate as inflateSync } from "pako"; +import { + Psd, + Layer, + ColorMode, + SectionDividerType, + LayerAdditionalInfo, + ReadOptions, + LayerMaskData, + Color, + PatternInfo, + GlobalLayerMaskInfo, + RGB, + PixelData, + PixelArray, + PostImageDataHandler, +} from "./psd"; +import { + resetImageData, + offsetForChannel, + decodeBitmap, + createImageData, + toBlendMode, + ChannelID, + Compression, + LayerMaskFlags, + MaskParams, + ColorSpace, + RAW_IMAGE_DATA, + largeAdditionalInfoKeys, + imageDataToCanvas, +} from "./helpers"; +import { infoHandlersMap } from "./additionalInfo"; +import { resourceHandlersMap } from "./imageResources"; interface ChannelInfo { - id: ChannelID; - length: number; + id: ChannelID; + length: number; } export interface ReadOptionsExt extends ReadOptions { - large: boolean; - globalAlpha: boolean; + large: boolean; + globalAlpha: boolean; } -export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB]; -const colorModes = ['bitmap', 'grayscale', 'indexed', 'RGB', 'CMYK', 'multichannel', 'duotone', 'lab']; +export const supportedColorModes = [ + ColorMode.Bitmap, + ColorMode.Grayscale, + ColorMode.RGB, +]; +const colorModes = [ + "bitmap", + "grayscale", + "indexed", + "RGB", + "CMYK", + "multichannel", + "duotone", + "lab", +]; function setupGrayscale(data: PixelData) { - const size = data.width * data.height * 4; + const size = data.width * data.height * 4; - for (let i = 0; i < size; i += 4) { - data.data[i + 1] = data.data[i]; - data.data[i + 2] = data.data[i]; - } + for (let i = 0; i < size; i += 4) { + data.data[i + 1] = data.data[i]; + data.data[i + 2] = data.data[i]; + } } export interface PsdReader { - offset: number; - view: DataView; - strict: boolean; - debug: boolean; + offset: number; + view: DataView; + strict: boolean; + debug: boolean; } -export function createReader(buffer: ArrayBuffer, offset?: number, length?: number): PsdReader { - const view = new DataView(buffer, offset, length); - return { view, offset: 0, strict: false, debug: false }; +export function createReader( + buffer: ArrayBuffer, + offset?: number, + length?: number +): PsdReader { + const view = new DataView(buffer, offset, length); + return { view, offset: 0, strict: false, debug: false }; } export function warnOrThrow(reader: PsdReader, message: string) { - if (reader.strict) throw new Error(message); - if (reader.debug) console.warn(message); + if (reader.strict) throw new Error(message); + if (reader.debug) console.warn(message); } export function readUint8(reader: PsdReader) { - reader.offset += 1; - return reader.view.getUint8(reader.offset - 1); + reader.offset += 1; + return reader.view.getUint8(reader.offset - 1); } export function peekUint8(reader: PsdReader) { - return reader.view.getUint8(reader.offset); + return reader.view.getUint8(reader.offset); } export function readInt16(reader: PsdReader) { - reader.offset += 2; - return reader.view.getInt16(reader.offset - 2, false); + reader.offset += 2; + return reader.view.getInt16(reader.offset - 2, false); } export function readUint16(reader: PsdReader) { - reader.offset += 2; - return reader.view.getUint16(reader.offset - 2, false); + reader.offset += 2; + return reader.view.getUint16(reader.offset - 2, false); } export function readUint16LE(reader: PsdReader) { - reader.offset += 2; - return reader.view.getUint16(reader.offset - 2, true); + reader.offset += 2; + return reader.view.getUint16(reader.offset - 2, true); } export function readInt32(reader: PsdReader) { - reader.offset += 4; - return reader.view.getInt32(reader.offset - 4, false); + reader.offset += 4; + return reader.view.getInt32(reader.offset - 4, false); } export function readInt32LE(reader: PsdReader) { - reader.offset += 4; - return reader.view.getInt32(reader.offset - 4, true); + reader.offset += 4; + return reader.view.getInt32(reader.offset - 4, true); } export function readUint32(reader: PsdReader) { - reader.offset += 4; - return reader.view.getUint32(reader.offset - 4, false); + reader.offset += 4; + return reader.view.getUint32(reader.offset - 4, false); } export function readFloat32(reader: PsdReader) { - reader.offset += 4; - return reader.view.getFloat32(reader.offset - 4, false); + reader.offset += 4; + return reader.view.getFloat32(reader.offset - 4, false); } export function readFloat64(reader: PsdReader) { - reader.offset += 8; - return reader.view.getFloat64(reader.offset - 8, false); + reader.offset += 8; + return reader.view.getFloat64(reader.offset - 8, false); } // 32-bit fixed-point number 16.16 export function readFixedPoint32(reader: PsdReader): number { - return readInt32(reader) / (1 << 16); + return readInt32(reader) / (1 << 16); } // 32-bit fixed-point number 8.24 export function readFixedPointPath32(reader: PsdReader): number { - return readInt32(reader) / (1 << 24); + return readInt32(reader) / (1 << 24); } export function readBytes(reader: PsdReader, length: number) { - const start = reader.view.byteOffset + reader.offset; - reader.offset += length; - - if ((start + length) > reader.view.buffer.byteLength) { - // fix for broken PSD files that are missing part of file at the end - warnOrThrow(reader, 'Reading bytes exceeding buffer length'); - if (length > (100 * 1024 * 1024)) throw new Error('Reading past end of file'); // limit to 100MB - const result = new Uint8Array(length); - const len = Math.min(length, reader.view.byteLength - start); - if (len > 0) result.set(new Uint8Array(reader.view.buffer, start, len)); - return result; - } else { - return new Uint8Array(reader.view.buffer, start, length); - } + const start = reader.view.byteOffset + reader.offset; + reader.offset += length; + + if (start + length > reader.view.buffer.byteLength) { + // fix for broken PSD files that are missing part of file at the end + warnOrThrow(reader, "Reading bytes exceeding buffer length"); + if (length > 100 * 1024 * 1024) throw new Error("Reading past end of file"); // limit to 100MB + const result = new Uint8Array(length); + const len = Math.min(length, reader.view.byteLength - start); + if (len > 0) result.set(new Uint8Array(reader.view.buffer, start, len)); + return result; + } else { + return new Uint8Array(reader.view.buffer, start, length); + } } export function readSignature(reader: PsdReader) { - return readShortString(reader, 4); + return readShortString(reader, 4); } export function readPascalString(reader: PsdReader, padTo: number) { - let length = readUint8(reader); - const text = length ? readShortString(reader, length) : ''; + let length = readUint8(reader); + const text = length ? readShortString(reader, length) : ""; - while (++length % padTo) { - reader.offset++; - } + while (++length % padTo) { + reader.offset++; + } - return text; + return text; } export function readUnicodeString(reader: PsdReader) { - const length = readUint32(reader); - return readUnicodeStringWithLength(reader, length); + const length = readUint32(reader); + return readUnicodeStringWithLength(reader, length); } export function readUnicodeStringWithLength(reader: PsdReader, length: number) { - let text = ''; + let text = ""; - while (length--) { - const value = readUint16(reader); + while (length--) { + const value = readUint16(reader); - if (value || length > 0) { // remove trailing \0 - text += String.fromCharCode(value); - } - } + if (value || length > 0) { + // remove trailing \0 + text += String.fromCharCode(value); + } + } - return text; + return text; } -export function readUnicodeStringWithLengthLE(reader: PsdReader, length: number) { - let text = ''; +export function readUnicodeStringWithLengthLE( + reader: PsdReader, + length: number +) { + let text = ""; - while (length--) { - const value = readUint16LE(reader); + while (length--) { + const value = readUint16LE(reader); - if (value || length > 0) { // remove trailing \0 - text += String.fromCharCode(value); - } - } + if (value || length > 0) { + // remove trailing \0 + text += String.fromCharCode(value); + } + } - return text; + return text; } export function readAsciiString(reader: PsdReader, length: number) { - let text = ''; + let text = ""; - while (length--) { - text += String.fromCharCode(readUint8(reader)); - } + while (length--) { + text += String.fromCharCode(readUint8(reader)); + } - return text; + return text; } export function skipBytes(reader: PsdReader, count: number) { - reader.offset += count; + reader.offset += count; } export function checkSignature(reader: PsdReader, a: string, b?: string) { - const offset = reader.offset; - const signature = readSignature(reader); - - if (signature !== a && signature !== b) { - throw new Error(`Invalid signature: '${signature}' at 0x${offset.toString(16)}`); - } + const offset = reader.offset; + const signature = readSignature(reader); + + if (signature !== a && signature !== b) { + throw new Error( + `Invalid signature: '${signature}' at 0x${offset.toString(16)}` + ); + } } function readShortString(reader: PsdReader, length: number) { - const buffer = readBytes(reader, length); - let result = ''; + const buffer = readBytes(reader, length); + let result = ""; - for (let i = 0; i < buffer.length; i++) { - result += String.fromCharCode(buffer[i]); - } + for (let i = 0; i < buffer.length; i++) { + result += String.fromCharCode(buffer[i]); + } - return result; + return result; } function isValidSignature(sig: string) { - return sig === '8BIM' || sig === 'MeSa' || sig === 'AgHg' || sig === 'PHUT' || sig === 'DCSR'; + return ( + sig === "8BIM" || + sig === "MeSa" || + sig === "AgHg" || + sig === "PHUT" || + sig === "DCSR" + ); } -export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) { - // header - checkSignature(reader, '8BPS'); - const version = readUint16(reader); - if (version !== 1 && version !== 2) throw new Error(`Invalid PSD file version: ${version}`); - - skipBytes(reader, 6); - const channels = readUint16(reader); - const height = readUint32(reader); - const width = readUint32(reader); - const bitsPerChannel = readUint16(reader); - const colorMode = readUint16(reader); - const maxSize = version === 1 ? 30000 : 300000; - - if (width > maxSize || height > maxSize) throw new Error(`Invalid size: ${width}x${height}`); - if (channels > 16) throw new Error(`Invalid channel count: ${channels}`); - if (![1, 8, 16, 32].includes(bitsPerChannel)) throw new Error(`Invalid bitsPerChannel: ${bitsPerChannel}`); - if (supportedColorModes.indexOf(colorMode) === -1) throw new Error(`Color mode not supported: ${colorModes[colorMode] ?? colorMode}`); - - const psd: Psd = { width, height, channels, bitsPerChannel, colorMode }; - const options: ReadOptionsExt = { ...readOptions, large: version === 2, globalAlpha: false }; - const fixOffsets = [0, 1, -1, 2, -2, 3, -3, 4, -4]; - - // color mode data - readSection(reader, 1, left => { - if (!left()) return; - - // const numbers: number[] = []; - // console.log('color mode', left()); - // while (left() > 0) { - // numbers.push(readUint32(reader)); - // } - // console.log('color mode', numbers); - - // if (options.throwForMissingFeatures) throw new Error('Color mode data not supported'); - skipBytes(reader, left()); - }); - - // image resources - readSection(reader, 1, left => { - while (left() > 0) { - const sigOffset = reader.offset; - let sig = ''; - - // attempt to fix broken document by realigning with the signature - for (const offset of fixOffsets) { - try { - reader.offset = sigOffset + offset; - sig = readSignature(reader); - } catch { } - if (isValidSignature(sig)) break; - } - - if (!isValidSignature(sig)) { - throw new Error(`Invalid signature: '${sig}' at 0x${(sigOffset).toString(16)}`); - } - - const id = readUint16(reader); - readPascalString(reader, 2); // name - - readSection(reader, 2, left => { - const handler = resourceHandlersMap[id]; - const skip = id === 1036 && !!options.skipThumbnail; - - if (!psd.imageResources) { - psd.imageResources = {}; - } - - if (handler && !skip) { - try { - handler.read(reader, psd.imageResources, left, options); - } catch (e) { - if (options.throwForMissingFeatures) throw e; - skipBytes(reader, left()); - } - } else { - // options.logMissingFeatures && console.log(`Unhandled image resource: ${id} (${left()})`); - skipBytes(reader, left()); - } - }); - } - }); - - // layer and mask info - readSection(reader, 1, left => { - readSection(reader, 2, left => { - readLayerInfo(reader, psd, options); - skipBytes(reader, left()); - }, undefined, options.large); - - // SAI does not include this section - if (left() > 0) { - const globalLayerMaskInfo = readGlobalLayerMaskInfo(reader); - if (globalLayerMaskInfo) psd.globalLayerMaskInfo = globalLayerMaskInfo; - } else { - // revert back to end of section if exceeded section limits - // opt.logMissingFeatures && console.log('reverting to end of section'); - skipBytes(reader, left()); - } - - while (left() > 0) { - // sometimes there are empty bytes here - while (left() && peekUint8(reader) === 0) { - // opt.logMissingFeatures && console.log('skipping 0 byte'); - skipBytes(reader, 1); - } - - if (left() >= 12) { - readAdditionalLayerInfo(reader, psd, psd, options); - } else { - // opt.logMissingFeatures && console.log('skipping leftover bytes', left()); - skipBytes(reader, left()); - } - } - }, undefined, options.large); - - const hasChildren = psd.children && psd.children.length; - const skipComposite = options.skipCompositeImageData && (options.skipLayerImageData || hasChildren); - - if (!skipComposite) { - readImageData(reader, psd, options); - } - - // TODO: show converted color mode instead of original PSD file color mode - // but add option to preserve file color mode (need to return image data instead of canvas in that case) - // psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB - - return psd; +export async function readPsd( + reader: PsdReader, + readOptions: ReadOptions = {}, + postImageDataHandler: PostImageDataHandler = async function ( + _data: PixelData, + _id?: number + ) {} +) { + // header + checkSignature(reader, "8BPS"); + const version = readUint16(reader); + if (version !== 1 && version !== 2) + throw new Error(`Invalid PSD file version: ${version}`); + + skipBytes(reader, 6); + const channels = readUint16(reader); + const height = readUint32(reader); + const width = readUint32(reader); + const bitsPerChannel = readUint16(reader); + const colorMode = readUint16(reader); + const maxSize = version === 1 ? 30000 : 300000; + + if (width > maxSize || height > maxSize) + throw new Error(`Invalid size: ${width}x${height}`); + if (channels > 16) throw new Error(`Invalid channel count: ${channels}`); + if (![1, 8, 16, 32].includes(bitsPerChannel)) + throw new Error(`Invalid bitsPerChannel: ${bitsPerChannel}`); + if (supportedColorModes.indexOf(colorMode) === -1) + throw new Error( + `Color mode not supported: ${colorModes[colorMode] ?? colorMode}` + ); + + const psd: Psd = { width, height, channels, bitsPerChannel, colorMode }; + const options: ReadOptionsExt = { + ...readOptions, + large: version === 2, + globalAlpha: false, + }; + const fixOffsets = [0, 1, -1, 2, -2, 3, -3, 4, -4]; + + // color mode data + await readSection(reader, 1, async (left) => { + if (!(await left())) return; + + // const numbers: number[] = []; + // console.log('color mode', await left()); + // while (await left() > 0) { + // numbers.push(readUint32(reader)); + // } + // console.log('color mode', numbers); + + // if (options.throwForMissingFeatures) throw new Error('Color mode data not supported'); + skipBytes(reader, await left()); + }); + + // image resources + await readSection(reader, 1, async (left) => { + while ((await left()) > 0) { + const sigOffset = reader.offset; + let sig = ""; + + // attempt to fix broken document by realigning with the signature + for (const offset of fixOffsets) { + try { + reader.offset = sigOffset + offset; + sig = readSignature(reader); + } catch {} + if (isValidSignature(sig)) break; + } + + if (!isValidSignature(sig)) { + throw new Error( + `Invalid signature: '${sig}' at 0x${sigOffset.toString(16)}` + ); + } + + const id = readUint16(reader); + readPascalString(reader, 2); // name + + await readSection(reader, 2, async (left) => { + const handler = resourceHandlersMap[id]; + const skip = id === 1036 && !!options.skipThumbnail; + + if (!psd.imageResources) { + psd.imageResources = {}; + } + + if (handler && !skip) { + try { + handler.read(reader, psd.imageResources, left, options); + } catch (e) { + if (options.throwForMissingFeatures) throw e; + skipBytes(reader, await left()); + } + } else { + // options.logMissingFeatures && console.log(`Unhandled image resource: ${id} (${await left()})`); + skipBytes(reader, await left()); + } + }); + } + }); + + // layer and mask info + await readSection( + reader, + 1, + async (left) => { + await readSection( + reader, + 2, + async (left) => { + await readLayerInfo(reader, psd, options, postImageDataHandler); + skipBytes(reader, await left()); + }, + undefined, + options.large + ); + + // SAI does not include this section + if ((await left()) > 0) { + const globalLayerMaskInfo = await readGlobalLayerMaskInfo(reader); + if (globalLayerMaskInfo) psd.globalLayerMaskInfo = globalLayerMaskInfo; + } else { + // revert back to end of section if exceeded section limits + // opt.logMissingFeatures && console.log('reverting to end of section'); + skipBytes(reader, await left()); + } + + while ((await left()) > 0) { + // sometimes there are empty bytes here + while ((await left()) && peekUint8(reader) === 0) { + // opt.logMissingFeatures && console.log('skipping 0 byte'); + skipBytes(reader, 1); + } + + if ((await left()) >= 12) { + await readAdditionalLayerInfo(reader, psd, psd, options); + } else { + // opt.logMissingFeatures && console.log('skipping leftover bytes', await left()); + skipBytes(reader, await left()); + } + } + }, + undefined, + options.large + ); + + const hasChildren = psd.children && psd.children.length; + const skipComposite = + options.skipCompositeImageData && + (options.skipLayerImageData || hasChildren); + + if (!skipComposite) { + await readImageData(reader, psd, options, postImageDataHandler); + } + + // TODO: show converted color mode instead of original PSD file color mode + // but add option to preserve file color mode (need to return image data instead of canvas in that case) + // psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB + + return psd; } -export function readLayerInfo(reader: PsdReader, psd: Psd, options: ReadOptionsExt) { - let layerCount = readInt16(reader); - - if (layerCount < 0) { - options.globalAlpha = true; - layerCount = -layerCount; - } - - const layers: Layer[] = []; - const layerChannels: ChannelInfo[][] = []; - - for (let i = 0; i < layerCount; i++) { - const { layer, channels } = readLayerRecord(reader, psd, options); - layers.push(layer); - layerChannels.push(channels); - } - - if (!options.skipLayerImageData) { - for (let i = 0; i < layerCount; i++) { - readLayerChannelImageData(reader, psd, layers[i], layerChannels[i], options); - } - } - - if (!psd.children) psd.children = []; - - const stack: (Layer | Psd)[] = [psd]; - - for (let i = layers.length - 1; i >= 0; i--) { - const l = layers[i]; - const type = l.sectionDivider ? l.sectionDivider.type : SectionDividerType.Other; - - if (type === SectionDividerType.OpenFolder || type === SectionDividerType.ClosedFolder) { - l.opened = type === SectionDividerType.OpenFolder; - l.children = []; - stack[stack.length - 1].children!.unshift(l); - stack.push(l); - } else if (type === SectionDividerType.BoundingSectionDivider) { - stack.pop(); - // this was workaround because I didn't know what `lsdk` section was, now it's probably not needed anymore - // } else if (l.name === '' && !l.sectionDivider && !l.top && !l.left && !l.bottom && !l.right) { - // // sometimes layer group terminator doesn't have sectionDivider, so we just guess here (PS bug ?) - // stack.pop(); - } else { - stack[stack.length - 1].children!.unshift(l); - } - } +export async function readLayerInfo( + reader: PsdReader, + psd: Psd, + options: ReadOptionsExt, + postImageDataHandler: PostImageDataHandler = async function ( + _data: PixelData, + _id?: number + ) {} +) { + let layerCount = readInt16(reader); + + if (layerCount < 0) { + options.globalAlpha = true; + layerCount = -layerCount; + } + + const layers: Layer[] = []; + const layerChannels: ChannelInfo[][] = []; + + for (let i = 0; i < layerCount; i++) { + const { layer, channels } = await readLayerRecord(reader, psd, options); + layers.push(layer); + layerChannels.push(channels); + } + + if (!options.skipLayerImageData) { + for (let i = 0; i < layerCount; i++) { + await readLayerChannelImageData( + reader, + psd, + layers[i], + layerChannels[i], + options, + postImageDataHandler + ); + } + } + + if (!psd.children) psd.children = []; + + const stack: (Layer | Psd)[] = [psd]; + + for (let i = layers.length - 1; i >= 0; i--) { + const l = layers[i]; + const type = l.sectionDivider + ? l.sectionDivider.type + : SectionDividerType.Other; + + if ( + type === SectionDividerType.OpenFolder || + type === SectionDividerType.ClosedFolder + ) { + l.opened = type === SectionDividerType.OpenFolder; + l.children = []; + stack[stack.length - 1].children!.unshift(l); + stack.push(l); + } else if (type === SectionDividerType.BoundingSectionDivider) { + stack.pop(); + // this was workaround because I didn't know what `lsdk` section was, now it's probably not needed anymore + // } else if (l.name === '' && !l.sectionDivider && !l.top && !l.left && !l.bottom && !l.right) { + // // sometimes layer group terminator doesn't have sectionDivider, so we just guess here (PS bug ?) + // stack.pop(); + } else { + stack[stack.length - 1].children!.unshift(l); + } + } } -function readLayerRecord(reader: PsdReader, psd: Psd, options: ReadOptionsExt) { - const layer: Layer = {}; - layer.top = readInt32(reader); - layer.left = readInt32(reader); - layer.bottom = readInt32(reader); - layer.right = readInt32(reader); - - const channelCount = readUint16(reader); - const channels: ChannelInfo[] = []; - - for (let i = 0; i < channelCount; i++) { - let id = readInt16(reader) as ChannelID; - let length = readUint32(reader); - - if (options.large) { - if (length !== 0) throw new Error('Sizes larger than 4GB are not supported'); - length = readUint32(reader); - } - - channels.push({ id, length }); - } - - checkSignature(reader, '8BIM'); - const blendMode = readSignature(reader); - if (!toBlendMode[blendMode]) throw new Error(`Invalid blend mode: '${blendMode}'`); - layer.blendMode = toBlendMode[blendMode]; - - layer.opacity = readUint8(reader) / 0xff; - layer.clipping = readUint8(reader) === 1; - - const flags = readUint8(reader); - layer.transparencyProtected = (flags & 0x01) !== 0; - layer.hidden = (flags & 0x02) !== 0; - if (flags & 0x20) layer.effectsOpen = true; - // 0x04 - obsolete - // 0x08 - 1 for Photoshop 5.0 and later, tells if bit 4 has useful information - // 0x10 - pixel data irrelevant to appearance of document - // 0x20 - effects/filters panel is expanded - - skipBytes(reader, 1); - - readSection(reader, 1, left => { - const mask = readLayerMaskData(reader, options); - if (mask) layer.mask = mask; - - /*const blendingRanges =*/ readLayerBlendingRanges(reader); - layer.name = readPascalString(reader, 4); - - while (left() > 0) { - readAdditionalLayerInfo(reader, layer, psd, options); - } - }); - - return { layer, channels }; +async function readLayerRecord( + reader: PsdReader, + psd: Psd, + options: ReadOptionsExt +) { + const layer: Layer = {}; + layer.top = readInt32(reader); + layer.left = readInt32(reader); + layer.bottom = readInt32(reader); + layer.right = readInt32(reader); + + const channelCount = readUint16(reader); + const channels: ChannelInfo[] = []; + + for (let i = 0; i < channelCount; i++) { + let id = readInt16(reader) as ChannelID; + let length = readUint32(reader); + + if (options.large) { + if (length !== 0) + throw new Error("Sizes larger than 4GB are not supported"); + length = readUint32(reader); + } + + channels.push({ id, length }); + } + + checkSignature(reader, "8BIM"); + const blendMode = readSignature(reader); + if (!toBlendMode[blendMode]) + throw new Error(`Invalid blend mode: '${blendMode}'`); + layer.blendMode = toBlendMode[blendMode]; + + layer.opacity = readUint8(reader) / 0xff; + layer.clipping = readUint8(reader) === 1; + + const flags = readUint8(reader); + layer.transparencyProtected = (flags & 0x01) !== 0; + layer.hidden = (flags & 0x02) !== 0; + if (flags & 0x20) layer.effectsOpen = true; + // 0x04 - obsolete + // 0x08 - 1 for Photoshop 5.0 and later, tells if bit 4 has useful information + // 0x10 - pixel data irrelevant to appearance of document + // 0x20 - effects/filters panel is expanded + + skipBytes(reader, 1); + + await readSection(reader, 1, async (left) => { + const mask = await readLayerMaskData(reader, options); + if (mask) layer.mask = mask; + + /*const blendingRanges =*/ await readLayerBlendingRanges(reader); + layer.name = readPascalString(reader, 4); + + while ((await left()) > 0) { + await readAdditionalLayerInfo(reader, layer, psd, options); + } + }); + + return { layer, channels }; } -function readLayerMaskData(reader: PsdReader, options: ReadOptions) { - return readSection(reader, 1, left => { - if (!left()) return undefined; - - const mask: LayerMaskData = {}; - mask.top = readInt32(reader); - mask.left = readInt32(reader); - mask.bottom = readInt32(reader); - mask.right = readInt32(reader); - mask.defaultColor = readUint8(reader); - - const flags = readUint8(reader); - mask.positionRelativeToLayer = (flags & LayerMaskFlags.PositionRelativeToLayer) !== 0; - mask.disabled = (flags & LayerMaskFlags.LayerMaskDisabled) !== 0; - mask.fromVectorData = (flags & LayerMaskFlags.LayerMaskFromRenderingOtherData) !== 0; - - if (flags & LayerMaskFlags.MaskHasParametersAppliedToIt) { - const params = readUint8(reader); - if (params & MaskParams.UserMaskDensity) mask.userMaskDensity = readUint8(reader) / 0xff; - if (params & MaskParams.UserMaskFeather) mask.userMaskFeather = readFloat64(reader); - if (params & MaskParams.VectorMaskDensity) mask.vectorMaskDensity = readUint8(reader) / 0xff; - if (params & MaskParams.VectorMaskFeather) mask.vectorMaskFeather = readFloat64(reader); - } - - if (left() > 2) { - // TODO: handle these values, this is RealUserMask - /*const realFlags = readUint8(reader); +async function readLayerMaskData(reader: PsdReader, options: ReadOptions) { + return readSection(reader, 1, async (left) => { + if (!(await left())) return undefined; + + const mask: LayerMaskData = {}; + mask.top = readInt32(reader); + mask.left = readInt32(reader); + mask.bottom = readInt32(reader); + mask.right = readInt32(reader); + mask.defaultColor = readUint8(reader); + + const flags = readUint8(reader); + mask.positionRelativeToLayer = + (flags & LayerMaskFlags.PositionRelativeToLayer) !== 0; + mask.disabled = (flags & LayerMaskFlags.LayerMaskDisabled) !== 0; + mask.fromVectorData = + (flags & LayerMaskFlags.LayerMaskFromRenderingOtherData) !== 0; + + if (flags & LayerMaskFlags.MaskHasParametersAppliedToIt) { + const params = readUint8(reader); + if (params & MaskParams.UserMaskDensity) + mask.userMaskDensity = readUint8(reader) / 0xff; + if (params & MaskParams.UserMaskFeather) + mask.userMaskFeather = readFloat64(reader); + if (params & MaskParams.VectorMaskDensity) + mask.vectorMaskDensity = readUint8(reader) / 0xff; + if (params & MaskParams.VectorMaskFeather) + mask.vectorMaskFeather = readFloat64(reader); + } + + if ((await left()) > 2) { + // TODO: handle these values, this is RealUserMask + /*const realFlags = readUint8(reader); const realUserMaskBackground = readUint8(reader); const top2 = readInt32(reader); const left2 = readInt32(reader); @@ -475,730 +599,1003 @@ function readLayerMaskData(reader: PsdReader, options: ReadOptions) { // TEMP (mask as any)._real = { realFlags, realUserMaskBackground, top2, left2, bottom2, right2 };*/ - if (options.logMissingFeatures) { - console.log('Unhandled extra reaal user mask params'); - } - } + if (options.logMissingFeatures) { + console.log("Unhandled extra reaal user mask params"); + } + } - skipBytes(reader, left()); - return mask; - }); + skipBytes(reader, await left()); + return mask; + }); } -function readLayerBlendingRanges(reader: PsdReader) { - return readSection(reader, 1, left => { - const compositeGrayBlendSource = readUint32(reader); - const compositeGraphBlendDestinationRange = readUint32(reader); - const ranges = []; - - while (left() > 0) { - const sourceRange = readUint32(reader); - const destRange = readUint32(reader); - ranges.push({ sourceRange, destRange }); - } - - return { compositeGrayBlendSource, compositeGraphBlendDestinationRange, ranges }; - }); +async function readLayerBlendingRanges(reader: PsdReader) { + return readSection(reader, 1, async (left) => { + const compositeGrayBlendSource = readUint32(reader); + const compositeGraphBlendDestinationRange = readUint32(reader); + const ranges = []; + + while ((await left()) > 0) { + const sourceRange = readUint32(reader); + const destRange = readUint32(reader); + ranges.push({ sourceRange, destRange }); + } + + return { + compositeGrayBlendSource, + compositeGraphBlendDestinationRange, + ranges, + }; + }); } -function readLayerChannelImageData(reader: PsdReader, psd: Psd, layer: Layer, channels: ChannelInfo[], options: ReadOptionsExt) { - const layerWidth = (layer.right || 0) - (layer.left || 0); - const layerHeight = (layer.bottom || 0) - (layer.top || 0); - const cmyk = psd.colorMode === ColorMode.CMYK; - - let imageData: PixelData | undefined; - - if (layerWidth && layerHeight) { - if (cmyk) { - if (psd.bitsPerChannel !== 8) throw new Error('bitsPerChannel Not supproted'); - imageData = { width: layerWidth, height: layerHeight, data: new Uint8ClampedArray(layerWidth * layerHeight * 5) } as any as ImageData; - for (let p = 4; p < imageData.data.byteLength; p += 5) imageData.data[p] = 255; - } else { - imageData = createImageDataBitDepth(layerWidth, layerHeight, psd.bitsPerChannel ?? 8); - resetImageData(imageData); - } - } - - if (RAW_IMAGE_DATA) (layer as any).imageDataRaw = []; - - for (const channel of channels) { - if (channel.length === 0) continue; - if (channel.length < 2) throw new Error('Invalid channel length'); - - const start = reader.offset; - - let compression = readUint16(reader) as Compression; - - // try to fix broken files where there's 1 byte shift of channel - if (compression > 3) { - reader.offset -= 1; - compression = readUint16(reader) as Compression; - } - - // try to fix broken files where there's 1 byte shift of channel - if (compression > 3) { - reader.offset -= 3; - compression = readUint16(reader) as Compression; - } - - if (compression > 3) throw new Error(`Invalid compression: ${compression}`); - - if (channel.id === ChannelID.UserMask) { - const mask = layer.mask; - - if (!mask) throw new Error(`Missing layer mask data`); - - const maskWidth = (mask.right || 0) - (mask.left || 0); - const maskHeight = (mask.bottom || 0) - (mask.top || 0); - - if (maskWidth && maskHeight) { - const maskData = createImageDataBitDepth(maskWidth, maskHeight, psd.bitsPerChannel ?? 8); - resetImageData(maskData); - - const start = reader.offset; - readData(reader, channel.length, maskData, compression, maskWidth, maskHeight, psd.bitsPerChannel ?? 8, 0, options.large, 4); - - if (RAW_IMAGE_DATA) { - (layer as any).maskDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start); - } - - setupGrayscale(maskData); - - if (options.useImageData) { - mask.imageData = maskData; - } else { - mask.canvas = imageDataToCanvas(maskData); - } - } - } else if (channel.id === ChannelID.RealUserMask) { - if (options.logMissingFeatures) { - console.log(`RealUserMask not supported`); - } - - reader.offset = start + channel.length; - } else { - const offset = offsetForChannel(channel.id, cmyk); - let targetData = imageData; - - if (offset < 0) { - targetData = undefined; - - if (options.throwForMissingFeatures) { - throw new Error(`Channel not supported: ${channel.id}`); - } - } - - readData(reader, channel.length, targetData, compression, layerWidth, layerHeight, psd.bitsPerChannel ?? 8, offset, options.large, cmyk ? 5 : 4); - - if (RAW_IMAGE_DATA) { - (layer as any).imageDataRaw[channel.id] = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start + 2, channel.length - 2); - } - - reader.offset = start + channel.length; - - if (targetData && psd.colorMode === ColorMode.Grayscale) { - setupGrayscale(targetData); - } - } - } - - if (imageData) { - if (cmyk) { - const cmykData = imageData; - imageData = createImageData(cmykData.width, cmykData.height); - cmykToRgb(cmykData, imageData, false); - } - - if (options.useImageData) { - layer.imageData = imageData; - } else { - layer.canvas = imageDataToCanvas(imageData); - } - } +async function readLayerChannelImageData( + reader: PsdReader, + psd: Psd, + layer: Layer, + channels: ChannelInfo[], + options: ReadOptionsExt, + postImageDataHandler: PostImageDataHandler = async function ( + _data: PixelData, + _id?: number + ) {} +) { + const layerWidth = (layer.right || 0) - (layer.left || 0); + const layerHeight = (layer.bottom || 0) - (layer.top || 0); + const cmyk = psd.colorMode === ColorMode.CMYK; + + let imageData: PixelData | undefined; + + if (layerWidth && layerHeight) { + if (cmyk) { + if (psd.bitsPerChannel !== 8) + throw new Error("bitsPerChannel Not supproted"); + imageData = { + width: layerWidth, + height: layerHeight, + data: new Uint8ClampedArray(layerWidth * layerHeight * 5), + } as any as ImageData; + for (let p = 4; p < imageData.data.byteLength; p += 5) + imageData.data[p] = 255; + } else { + imageData = createImageDataBitDepth( + layerWidth, + layerHeight, + psd.bitsPerChannel ?? 8 + ); + resetImageData(imageData); + } + } + + if (RAW_IMAGE_DATA) (layer as any).imageDataRaw = []; + + for (const channel of channels) { + if (channel.length === 0) continue; + if (channel.length < 2) throw new Error("Invalid channel length"); + + const start = reader.offset; + + let compression = readUint16(reader) as Compression; + + // try to fix broken files where there's 1 byte shift of channel + if (compression > 3) { + reader.offset -= 1; + compression = readUint16(reader) as Compression; + } + + // try to fix broken files where there's 1 byte shift of channel + if (compression > 3) { + reader.offset -= 3; + compression = readUint16(reader) as Compression; + } + + if (compression > 3) throw new Error(`Invalid compression: ${compression}`); + + if (channel.id === ChannelID.UserMask) { + const mask = layer.mask; + + if (!mask) throw new Error(`Missing layer mask data`); + + const maskWidth = (mask.right || 0) - (mask.left || 0); + const maskHeight = (mask.bottom || 0) - (mask.top || 0); + + if (maskWidth && maskHeight) { + const maskData = createImageDataBitDepth( + maskWidth, + maskHeight, + psd.bitsPerChannel ?? 8 + ); + resetImageData(maskData); + + const start = reader.offset; + readData( + reader, + channel.length, + maskData, + compression, + maskWidth, + maskHeight, + psd.bitsPerChannel ?? 8, + 0, + options.large, + 4 + ); + + if (RAW_IMAGE_DATA) { + (layer as any).maskDataRaw = new Uint8Array( + reader.view.buffer, + reader.view.byteOffset + start, + reader.offset - start + ); + } + + setupGrayscale(maskData); + + if (options.useImageData) { + mask.imageData = maskData; + } else if (options.useCanvasData) { + mask.canvas = imageDataToCanvas(maskData); + } + + await postImageDataHandler(maskData, layer.id); + } + } else if (channel.id === ChannelID.RealUserMask) { + if (options.logMissingFeatures) { + console.log(`RealUserMask not supported`); + } + + reader.offset = start + channel.length; + } else { + const offset = offsetForChannel(channel.id, cmyk); + let targetData = imageData; + + if (offset < 0) { + targetData = undefined; + + if (options.throwForMissingFeatures) { + throw new Error(`Channel not supported: ${channel.id}`); + } + } + + readData( + reader, + channel.length, + targetData, + compression, + layerWidth, + layerHeight, + psd.bitsPerChannel ?? 8, + offset, + options.large, + cmyk ? 5 : 4 + ); + + if (RAW_IMAGE_DATA) { + (layer as any).imageDataRaw[channel.id] = new Uint8Array( + reader.view.buffer, + reader.view.byteOffset + start + 2, + channel.length - 2 + ); + } + + reader.offset = start + channel.length; + + if (targetData && psd.colorMode === ColorMode.Grayscale) { + setupGrayscale(targetData); + } + } + } + + if (imageData) { + if (cmyk) { + const cmykData = imageData; + imageData = createImageData(cmykData.width, cmykData.height); + cmykToRgb(cmykData, imageData, false); + } + + if (options.useImageData) { + layer.imageData = imageData; + } else if (options.useCanvasData) { + layer.canvas = imageDataToCanvas(imageData); + } + + await postImageDataHandler(imageData, layer.id); + } } -function readData(reader: PsdReader, length: number, data: PixelData | undefined, compression: Compression, width: number, height: number, bitDepth: number, offset: number, large: boolean, step: number) { - if (compression === Compression.RawData) { - readDataRaw(reader, data, width, height, bitDepth, step, offset); - } else if (compression === Compression.RleCompressed) { - readDataRLE(reader, data, width, height, bitDepth, step, [offset], large); - } else if (compression === Compression.ZipWithoutPrediction) { - readDataZip(reader, length, data, width, height, bitDepth, step, offset, false); - } else if (compression === Compression.ZipWithPrediction) { - readDataZip(reader, length, data, width, height, bitDepth, step, offset, true); - } else { - throw new Error(`Invalid Compression type: ${compression}`); - } +function readData( + reader: PsdReader, + length: number, + data: PixelData | undefined, + compression: Compression, + width: number, + height: number, + bitDepth: number, + offset: number, + large: boolean, + step: number +) { + if (compression === Compression.RawData) { + readDataRaw(reader, data, width, height, bitDepth, step, offset); + } else if (compression === Compression.RleCompressed) { + readDataRLE(reader, data, width, height, bitDepth, step, [offset], large); + } else if (compression === Compression.ZipWithoutPrediction) { + readDataZip( + reader, + length, + data, + width, + height, + bitDepth, + step, + offset, + false + ); + } else if (compression === Compression.ZipWithPrediction) { + readDataZip( + reader, + length, + data, + width, + height, + bitDepth, + step, + offset, + true + ); + } else { + throw new Error(`Invalid Compression type: ${compression}`); + } } -export function readGlobalLayerMaskInfo(reader: PsdReader) { - return readSection(reader, 1, left => { - if (!left()) return undefined; - - const overlayColorSpace = readUint16(reader); - const colorSpace1 = readUint16(reader); - const colorSpace2 = readUint16(reader); - const colorSpace3 = readUint16(reader); - const colorSpace4 = readUint16(reader); - const opacity = readUint16(reader) / 0xff; - const kind = readUint8(reader); - skipBytes(reader, left()); // 3 bytes of padding ? - return { overlayColorSpace, colorSpace1, colorSpace2, colorSpace3, colorSpace4, opacity, kind }; - }); +export async function readGlobalLayerMaskInfo(reader: PsdReader) { + return readSection( + reader, + 1, + async (left) => { + if (!(await left())) return undefined; + + const overlayColorSpace = readUint16(reader); + const colorSpace1 = readUint16(reader); + const colorSpace2 = readUint16(reader); + const colorSpace3 = readUint16(reader); + const colorSpace4 = readUint16(reader); + const opacity = readUint16(reader) / 0xff; + const kind = readUint8(reader); + skipBytes(reader, await left()); // 3 bytes of padding ? + return { + overlayColorSpace, + colorSpace1, + colorSpace2, + colorSpace3, + colorSpace4, + opacity, + kind, + }; + } + ); } -export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAdditionalInfo, psd: Psd, options: ReadOptionsExt) { - const sig = readSignature(reader); - if (sig !== '8BIM' && sig !== '8B64') throw new Error(`Invalid signature: '${sig}' at 0x${(reader.offset - 4).toString(16)}`); - const key = readSignature(reader); - - // `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit - const u64 = sig === '8B64' || (options.large && largeAdditionalInfoKeys.indexOf(key) !== -1); - - readSection(reader, 2, left => { - const handler = infoHandlersMap[key]; - - if (handler) { - try { - handler.read(reader, target, left, psd, options); - } catch (e) { - if (options.throwForMissingFeatures) throw e; - } - } else { - options.logMissingFeatures && console.log(`Unhandled additional info: ${key}`); - skipBytes(reader, left()); - } - - if (left()) { - options.logMissingFeatures && console.log(`Unread ${left()} bytes left for additional info: ${key}`); - skipBytes(reader, left()); - } - }, false, u64); +export async function readAdditionalLayerInfo( + reader: PsdReader, + target: LayerAdditionalInfo, + psd: Psd, + options: ReadOptionsExt +) { + const sig = readSignature(reader); + if (sig !== "8BIM" && sig !== "8B64") + throw new Error( + `Invalid signature: '${sig}' at 0x${(reader.offset - 4).toString(16)}` + ); + const key = readSignature(reader); + + // `largeAdditionalInfoKeys` fallback, because some keys don't have 8B64 signature even when they are 64bit + const u64 = + sig === "8B64" || + (options.large && largeAdditionalInfoKeys.indexOf(key) !== -1); + + await readSection( + reader, + 2, + async (left) => { + const handler = infoHandlersMap[key]; + + if (handler) { + try { + await handler.read(reader, target, left, psd, options); + } catch (e) { + if (options.throwForMissingFeatures) throw e; + } + } else { + options.logMissingFeatures && + console.log(`Unhandled additional info: ${key}`); + skipBytes(reader, await left()); + } + + if (await left()) { + options.logMissingFeatures && + console.log( + `Unread ${await left()} bytes left for additional info: ${key}` + ); + skipBytes(reader, await left()); + } + }, + false, + u64 + ); } -function createImageDataBitDepth(width: number, height: number, bitDepth: number): PixelData { - if (bitDepth === 1 || bitDepth === 8) { - return createImageData(width, height); - } else if (bitDepth === 16) { - return { width, height, data: new Uint16Array(width * height * 4) }; - } else if (bitDepth === 32) { - return { width, height, data: new Float32Array(width * height * 4) }; - } else { - throw new Error(`Invalid bitDepth (${bitDepth})`); - } +function createImageDataBitDepth( + width: number, + height: number, + bitDepth: number +): PixelData { + if (bitDepth === 1 || bitDepth === 8) { + return createImageData(width, height); + } else if (bitDepth === 16) { + return { width, height, data: new Uint16Array(width * height * 4) }; + } else if (bitDepth === 32) { + return { width, height, data: new Float32Array(width * height * 4) }; + } else { + throw new Error(`Invalid bitDepth (${bitDepth})`); + } } -function readImageData(reader: PsdReader, psd: Psd, options: ReadOptionsExt) { - const compression = readUint16(reader) as Compression; - const bitsPerChannel = psd.bitsPerChannel ?? 8; - - if (supportedColorModes.indexOf(psd.colorMode!) === -1) - throw new Error(`Color mode not supported: ${psd.colorMode}`); - - if (compression !== Compression.RawData && compression !== Compression.RleCompressed) - throw new Error(`Compression type not supported: ${compression}`); - - const imageData = createImageDataBitDepth(psd.width, psd.height, bitsPerChannel); - resetImageData(imageData); - - switch (psd.colorMode) { - case ColorMode.Bitmap: { - if (bitsPerChannel !== 1) throw new Error('Invalid bitsPerChannel for bitmap color mode'); - - let bytes: Uint8Array; - - if (compression === Compression.RawData) { - bytes = readBytes(reader, Math.ceil(psd.width / 8) * psd.height); - } else if (compression === Compression.RleCompressed) { - bytes = new Uint8Array(psd.width * psd.height); - readDataRLE(reader, { data: bytes, width: psd.width, height: psd.height }, psd.width, psd.height, 8, 1, [0], options.large); - } else { - throw new Error(`Bitmap compression not supported: ${compression}`); - } - - decodeBitmap(bytes, imageData.data, psd.width, psd.height); - break; - } - case ColorMode.RGB: - case ColorMode.Grayscale: { - const channels = psd.colorMode === ColorMode.Grayscale ? [0] : [0, 1, 2]; - - if (psd.channels && psd.channels > 3) { - for (let i = 3; i < psd.channels; i++) { - // TODO: store these channels in additional image data - channels.push(i); - } - } else if (options.globalAlpha) { - channels.push(3); - } - - if (compression === Compression.RawData) { - for (let i = 0; i < channels.length; i++) { - readDataRaw(reader, imageData, psd.width, psd.height, bitsPerChannel, 4, channels[i]); - } - } else if (compression === Compression.RleCompressed) { - const start = reader.offset; - readDataRLE(reader, imageData, psd.width, psd.height, bitsPerChannel, 4, channels, options.large); - if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start); - } - - if (psd.colorMode === ColorMode.Grayscale) { - setupGrayscale(imageData); - } - break; - } - case ColorMode.CMYK: { - if (psd.bitsPerChannel !== 8) throw new Error('bitsPerChannel Not supproted'); - if (psd.channels !== 4) throw new Error(`Invalid channel count`); - - const channels = [0, 1, 2, 3]; - if (options.globalAlpha) channels.push(4); - - if (compression === Compression.RawData) { - throw new Error(`Not implemented`); - // TODO: ... - // for (let i = 0; i < channels.length; i++) { - // readDataRaw(reader, imageData, channels[i], psd.width, psd.height); - // } - } else if (compression === Compression.RleCompressed) { - const cmykImageData: PixelData = { - width: imageData.width, - height: imageData.height, - data: new Uint8Array(imageData.width * imageData.height * 5), - }; - - const start = reader.offset; - readDataRLE(reader, cmykImageData, psd.width, psd.height, psd.bitsPerChannel ?? 8, 5, channels, options.large); - cmykToRgb(cmykImageData, imageData, true); - - if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start); - } - - break; - } - default: throw new Error(`Color mode not supported: ${psd.colorMode}`); - } - - // remove weird white matte - if (options.globalAlpha) { - if (psd.bitsPerChannel !== 8) throw new Error('bitsPerChannel Not supproted'); - const p = imageData.data; - const size = imageData.width * imageData.height * 4; - for (let i = 0; i < size; i += 4) { - const pa = p[i + 3]; - if (pa != 0 && pa != 255) { - const a = pa / 255; - const ra = 1 / a; - const invA = 255 * (1 - ra); - p[i + 0] = p[i + 0] * ra + invA; - p[i + 1] = p[i + 1] * ra + invA; - p[i + 2] = p[i + 2] * ra + invA; - } - } - } - - if (options.useImageData) { - psd.imageData = imageData; - } else { - psd.canvas = imageDataToCanvas(imageData); - } +async function readImageData( + reader: PsdReader, + psd: Psd, + options: ReadOptionsExt, + postImageDataHandler: PostImageDataHandler = async function ( + _data: PixelData, + _id?: number + ) {} +) { + const compression = readUint16(reader) as Compression; + const bitsPerChannel = psd.bitsPerChannel ?? 8; + + if (supportedColorModes.indexOf(psd.colorMode!) === -1) + throw new Error(`Color mode not supported: ${psd.colorMode}`); + + if ( + compression !== Compression.RawData && + compression !== Compression.RleCompressed + ) + throw new Error(`Compression type not supported: ${compression}`); + + const imageData = createImageDataBitDepth( + psd.width, + psd.height, + bitsPerChannel + ); + resetImageData(imageData); + + switch (psd.colorMode) { + case ColorMode.Bitmap: { + if (bitsPerChannel !== 1) + throw new Error("Invalid bitsPerChannel for bitmap color mode"); + + let bytes: Uint8Array; + + if (compression === Compression.RawData) { + bytes = readBytes(reader, Math.ceil(psd.width / 8) * psd.height); + } else if (compression === Compression.RleCompressed) { + bytes = new Uint8Array(psd.width * psd.height); + readDataRLE( + reader, + { data: bytes, width: psd.width, height: psd.height }, + psd.width, + psd.height, + 8, + 1, + [0], + options.large + ); + } else { + throw new Error(`Bitmap compression not supported: ${compression}`); + } + + decodeBitmap(bytes, imageData.data, psd.width, psd.height); + break; + } + case ColorMode.RGB: + case ColorMode.Grayscale: { + const channels = psd.colorMode === ColorMode.Grayscale ? [0] : [0, 1, 2]; + + if (psd.channels && psd.channels > 3) { + for (let i = 3; i < psd.channels; i++) { + // TODO: store these channels in additional image data + channels.push(i); + } + } else if (options.globalAlpha) { + channels.push(3); + } + + if (compression === Compression.RawData) { + for (let i = 0; i < channels.length; i++) { + readDataRaw( + reader, + imageData, + psd.width, + psd.height, + bitsPerChannel, + 4, + channels[i] + ); + } + } else if (compression === Compression.RleCompressed) { + const start = reader.offset; + readDataRLE( + reader, + imageData, + psd.width, + psd.height, + bitsPerChannel, + 4, + channels, + options.large + ); + if (RAW_IMAGE_DATA) + (psd as any).imageDataRaw = new Uint8Array( + reader.view.buffer, + reader.view.byteOffset + start, + reader.offset - start + ); + } + + if (psd.colorMode === ColorMode.Grayscale) { + setupGrayscale(imageData); + } + break; + } + case ColorMode.CMYK: { + if (psd.bitsPerChannel !== 8) + throw new Error("bitsPerChannel Not supproted"); + if (psd.channels !== 4) throw new Error(`Invalid channel count`); + + const channels = [0, 1, 2, 3]; + if (options.globalAlpha) channels.push(4); + + if (compression === Compression.RawData) { + throw new Error(`Not implemented`); + // TODO: ... + // for (let i = 0; i < channels.length; i++) { + // readDataRaw(reader, imageData, channels[i], psd.width, psd.height); + // } + } else if (compression === Compression.RleCompressed) { + const cmykImageData: PixelData = { + width: imageData.width, + height: imageData.height, + data: new Uint8Array(imageData.width * imageData.height * 5), + }; + + const start = reader.offset; + readDataRLE( + reader, + cmykImageData, + psd.width, + psd.height, + psd.bitsPerChannel ?? 8, + 5, + channels, + options.large + ); + cmykToRgb(cmykImageData, imageData, true); + + if (RAW_IMAGE_DATA) + (psd as any).imageDataRaw = new Uint8Array( + reader.view.buffer, + reader.view.byteOffset + start, + reader.offset - start + ); + } + + break; + } + default: + throw new Error(`Color mode not supported: ${psd.colorMode}`); + } + + // remove weird white matte + if (options.globalAlpha) { + if (psd.bitsPerChannel !== 8) + throw new Error("bitsPerChannel Not supproted"); + const p = imageData.data; + const size = imageData.width * imageData.height * 4; + for (let i = 0; i < size; i += 4) { + const pa = p[i + 3]; + if (pa != 0 && pa != 255) { + const a = pa / 255; + const ra = 1 / a; + const invA = 255 * (1 - ra); + p[i + 0] = p[i + 0] * ra + invA; + p[i + 1] = p[i + 1] * ra + invA; + p[i + 2] = p[i + 2] * ra + invA; + } + } + } + + if (options.useImageData) { + psd.imageData = imageData; + } else if (options.useCanvasData) { + psd.canvas = imageDataToCanvas(imageData); + } + + await postImageDataHandler(imageData, -1); } function cmykToRgb(cmyk: PixelData, rgb: PixelData, reverseAlpha: boolean) { - const size = rgb.width * rgb.height * 4; - const srcData = cmyk.data; - const dstData = rgb.data; - - for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) { - const c = srcData[src]; - const m = srcData[src + 1]; - const y = srcData[src + 2]; - const k = srcData[src + 3]; - dstData[dst] = ((((c * k) | 0) / 255) | 0); - dstData[dst + 1] = ((((m * k) | 0) / 255) | 0); - dstData[dst + 2] = ((((y * k) | 0) / 255) | 0); - dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4]; - } - - // for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) { - // const c = 1 - (srcData[src + 0] / 255); - // const m = 1 - (srcData[src + 1] / 255); - // const y = 1 - (srcData[src + 2] / 255); - // // const k = srcData[src + 3] / 255; - // dstData[dst + 0] = ((1 - c * 0.8) * 255) | 0; - // dstData[dst + 1] = ((1 - m * 0.8) * 255) | 0; - // dstData[dst + 2] = ((1 - y * 0.8) * 255) | 0; - // dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4]; - // } + const size = rgb.width * rgb.height * 4; + const srcData = cmyk.data; + const dstData = rgb.data; + + for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) { + const c = srcData[src]; + const m = srcData[src + 1]; + const y = srcData[src + 2]; + const k = srcData[src + 3]; + dstData[dst] = (((c * k) | 0) / 255) | 0; + dstData[dst + 1] = (((m * k) | 0) / 255) | 0; + dstData[dst + 2] = (((y * k) | 0) / 255) | 0; + dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4]; + } + + // for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) { + // const c = 1 - (srcData[src + 0] / 255); + // const m = 1 - (srcData[src + 1] / 255); + // const y = 1 - (srcData[src + 2] / 255); + // // const k = srcData[src + 3] / 255; + // dstData[dst + 0] = ((1 - c * 0.8) * 255) | 0; + // dstData[dst + 1] = ((1 - m * 0.8) * 255) | 0; + // dstData[dst + 2] = ((1 - y * 0.8) * 255) | 0; + // dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4]; + // } } function verifyCompatible(a: PixelArray, b: PixelArray) { - if ((a.byteLength / a.length) !== (b.byteLength / b.length)) { - throw new Error('Invalid array types'); - } + if (a.byteLength / a.length !== b.byteLength / b.length) { + throw new Error("Invalid array types"); + } } function bytesToArray(bytes: Uint8Array, bitDepth: number) { - if (bitDepth === 8) { - return bytes; - } else if (bitDepth === 16) { - if (bytes.byteOffset % 2) { - const result = new Uint16Array(bytes.byteLength / 2); - new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set(bytes); - return result; - } else { - return new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2); - } - } else if (bitDepth === 32) { - if (bytes.byteOffset % 4) { - const result = new Float32Array(bytes.byteLength / 4); - new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set(bytes); - return result; - } else { - return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4); - } - } else { - throw new Error(`Invalid bitDepth (${bitDepth})`) - } + if (bitDepth === 8) { + return bytes; + } else if (bitDepth === 16) { + if (bytes.byteOffset % 2) { + const result = new Uint16Array(bytes.byteLength / 2); + new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set( + bytes + ); + return result; + } else { + return new Uint16Array( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength / 2 + ); + } + } else if (bitDepth === 32) { + if (bytes.byteOffset % 4) { + const result = new Float32Array(bytes.byteLength / 4); + new Uint8Array(result.buffer, result.byteOffset, result.byteLength).set( + bytes + ); + return result; + } else { + return new Float32Array( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength / 4 + ); + } + } else { + throw new Error(`Invalid bitDepth (${bitDepth})`); + } } -function copyChannelToPixelData(pixelData: PixelData, channel: PixelArray, offset: number, step: number) { - verifyCompatible(pixelData.data, channel); - const size = pixelData.width * pixelData.height; - const data = pixelData.data; - for (let i = 0, p = offset | 0; i < size; i++, p = (p + step) | 0) { - data[p] = channel[i]; - } +function copyChannelToPixelData( + pixelData: PixelData, + channel: PixelArray, + offset: number, + step: number +) { + verifyCompatible(pixelData.data, channel); + const size = pixelData.width * pixelData.height; + const data = pixelData.data; + for (let i = 0, p = offset | 0; i < size; i++, p = (p + step) | 0) { + data[p] = channel[i]; + } } -function readDataRaw(reader: PsdReader, pixelData: PixelData | undefined, width: number, height: number, bitDepth: number, step: number, offset: number) { - const buffer = readBytes(reader, width * height * Math.floor(bitDepth / 8)); - - if (bitDepth == 32) { - for (let i = 0; i < buffer.byteLength; i += 4) { - const a = buffer[i + 0]; - const b = buffer[i + 1]; - const c = buffer[i + 2]; - const d = buffer[i + 3]; - buffer[i + 0] = d; - buffer[i + 1] = c; - buffer[i + 2] = b; - buffer[i + 3] = a; - } - } - - const array = bytesToArray(buffer, bitDepth); - - if (pixelData && offset < step) { - copyChannelToPixelData(pixelData, array, offset, step); - } +function readDataRaw( + reader: PsdReader, + pixelData: PixelData | undefined, + width: number, + height: number, + bitDepth: number, + step: number, + offset: number +) { + const buffer = readBytes(reader, width * height * Math.floor(bitDepth / 8)); + + if (bitDepth == 32) { + for (let i = 0; i < buffer.byteLength; i += 4) { + const a = buffer[i + 0]; + const b = buffer[i + 1]; + const c = buffer[i + 2]; + const d = buffer[i + 3]; + buffer[i + 0] = d; + buffer[i + 1] = c; + buffer[i + 2] = b; + buffer[i + 3] = a; + } + } + + const array = bytesToArray(buffer, bitDepth); + + if (pixelData && offset < step) { + copyChannelToPixelData(pixelData, array, offset, step); + } } -function decodePredicted(data: Uint8Array | Uint16Array, width: number, height: number, mod: number) { - for (let y = 0; y < height; y++) { - const offset = y * width; - - for (let x = 1, o = offset + 1; x < width; x++, o++) { - data[o] = (data[o - 1] + data[o]) % mod; - } - } +function decodePredicted( + data: Uint8Array | Uint16Array, + width: number, + height: number, + mod: number +) { + for (let y = 0; y < height; y++) { + const offset = y * width; + + for (let x = 1, o = offset + 1; x < width; x++, o++) { + data[o] = (data[o - 1] + data[o]) % mod; + } + } } -export function readDataZip(reader: PsdReader, length: number, pixelData: PixelData | undefined, width: number, height: number, bitDepth: number, step: number, offset: number, prediction: boolean) { - const compressed = readBytes(reader, length); - const decompressed = inflateSync(compressed); - - if (pixelData && offset < step) { - const array = bytesToArray(decompressed, bitDepth); - - if (bitDepth === 8) { - if (prediction) decodePredicted(decompressed, width, height, 0x100); - copyChannelToPixelData(pixelData, decompressed, offset, step); - } else if (bitDepth === 16) { - if (prediction) decodePredicted(array as Uint16Array, width, height, 0x10000); - copyChannelToPixelData(pixelData, array, offset, step); - } else if (bitDepth === 32) { - if (prediction) decodePredicted(decompressed, width * 4, height, 0x100); - - let di = offset; - const dst = new Uint32Array(pixelData.data.buffer, pixelData.data.byteOffset, pixelData.data.length); - - for (let y = 0; y < height; y++) { - let a = width * 4 * y; - - for (let x = 0; x < width; x++, a++, di += step) { - const b = a + width; - const c = b + width; - const d = c + width; - dst[di] = ((decompressed[a] << 24) | (decompressed[b] << 16) | (decompressed[c] << 8) | decompressed[d]) >>> 0; - } - } - } else { - throw new Error('Invalid bitDepth'); - } - } +export function readDataZip( + reader: PsdReader, + length: number, + pixelData: PixelData | undefined, + width: number, + height: number, + bitDepth: number, + step: number, + offset: number, + prediction: boolean +) { + const compressed = readBytes(reader, length); + const decompressed = inflateSync(compressed); + + if (pixelData && offset < step) { + const array = bytesToArray(decompressed, bitDepth); + + if (bitDepth === 8) { + if (prediction) decodePredicted(decompressed, width, height, 0x100); + copyChannelToPixelData(pixelData, decompressed, offset, step); + } else if (bitDepth === 16) { + if (prediction) + decodePredicted(array as Uint16Array, width, height, 0x10000); + copyChannelToPixelData(pixelData, array, offset, step); + } else if (bitDepth === 32) { + if (prediction) decodePredicted(decompressed, width * 4, height, 0x100); + + let di = offset; + const dst = new Uint32Array( + pixelData.data.buffer, + pixelData.data.byteOffset, + pixelData.data.length + ); + + for (let y = 0; y < height; y++) { + let a = width * 4 * y; + + for (let x = 0; x < width; x++, a++, di += step) { + const b = a + width; + const c = b + width; + const d = c + width; + dst[di] = + ((decompressed[a] << 24) | + (decompressed[b] << 16) | + (decompressed[c] << 8) | + decompressed[d]) >>> + 0; + } + } + } else { + throw new Error("Invalid bitDepth"); + } + } } -export function readDataRLE(reader: PsdReader, pixelData: PixelData | undefined, _width: number, height: number, bitDepth: number, step: number, offsets: number[], large: boolean) { - const data = pixelData && pixelData.data; - let lengths: Uint16Array | Uint32Array; - - if (large) { - lengths = new Uint32Array(offsets.length * height); - - for (let o = 0, li = 0; o < offsets.length; o++) { - for (let y = 0; y < height; y++, li++) { - lengths[li] = readUint32(reader); - } - } - } else { - lengths = new Uint16Array(offsets.length * height); - - for (let o = 0, li = 0; o < offsets.length; o++) { - for (let y = 0; y < height; y++, li++) { - lengths[li] = readUint16(reader); - } - } - } - - if (bitDepth !== 1 && bitDepth !== 8) throw new Error(`Invalid bit depth (${bitDepth})`); - - const extraLimit = (step - 1) | 0; // 3 for rgb, 4 for cmyk - - for (let c = 0, li = 0; c < offsets.length; c++) { - const offset = offsets[c] | 0; - const extra = c > extraLimit || offset > extraLimit; - - if (!data || extra) { - for (let y = 0; y < height; y++, li++) { - skipBytes(reader, lengths[li]); - } - } else { - for (let y = 0, p = offset | 0; y < height; y++, li++) { - const length = lengths[li]; - const buffer = readBytes(reader, length); - - for (let i = 0; i < length; i++) { - let header = buffer[i]; - - if (header > 128) { - const value = buffer[++i]; - header = (256 - header) | 0; - - for (let j = 0; j <= header; j = (j + 1) | 0) { - data[p] = value; - p = (p + step) | 0; - } - } else if (header < 128) { - for (let j = 0; j <= header; j = (j + 1) | 0) { - data[p] = buffer[++i]; - p = (p + step) | 0; - } - } else { - // ignore 128 - } - - // This showed up on some images from non-photoshop programs, ignoring it seems to work just fine. - // if (i >= length) throw new Error(`Invalid RLE data: exceeded buffer size ${i}/${length}`); - } - } - } - } +export function readDataRLE( + reader: PsdReader, + pixelData: PixelData | undefined, + _width: number, + height: number, + bitDepth: number, + step: number, + offsets: number[], + large: boolean +) { + const data = pixelData && pixelData.data; + let lengths: Uint16Array | Uint32Array; + + if (large) { + lengths = new Uint32Array(offsets.length * height); + + for (let o = 0, li = 0; o < offsets.length; o++) { + for (let y = 0; y < height; y++, li++) { + lengths[li] = readUint32(reader); + } + } + } else { + lengths = new Uint16Array(offsets.length * height); + + for (let o = 0, li = 0; o < offsets.length; o++) { + for (let y = 0; y < height; y++, li++) { + lengths[li] = readUint16(reader); + } + } + } + + if (bitDepth !== 1 && bitDepth !== 8) + throw new Error(`Invalid bit depth (${bitDepth})`); + + const extraLimit = (step - 1) | 0; // 3 for rgb, 4 for cmyk + + for (let c = 0, li = 0; c < offsets.length; c++) { + const offset = offsets[c] | 0; + const extra = c > extraLimit || offset > extraLimit; + + if (!data || extra) { + for (let y = 0; y < height; y++, li++) { + skipBytes(reader, lengths[li]); + } + } else { + for (let y = 0, p = offset | 0; y < height; y++, li++) { + const length = lengths[li]; + const buffer = readBytes(reader, length); + + for (let i = 0; i < length; i++) { + let header = buffer[i]; + + if (header > 128) { + const value = buffer[++i]; + header = (256 - header) | 0; + + for (let j = 0; j <= header; j = (j + 1) | 0) { + data[p] = value; + p = (p + step) | 0; + } + } else if (header < 128) { + for (let j = 0; j <= header; j = (j + 1) | 0) { + data[p] = buffer[++i]; + p = (p + step) | 0; + } + } else { + // ignore 128 + } + + // This showed up on some images from non-photoshop programs, ignoring it seems to work just fine. + // if (i >= length) throw new Error(`Invalid RLE data: exceeded buffer size ${i}/${length}`); + } + } + } + } } -export function readSection( - reader: PsdReader, round: number, func: (left: () => number) => T, skipEmpty = true, eightBytes = false -): T | undefined { - let length = readUint32(reader); - - if (eightBytes) { - if (length !== 0) throw new Error('Sizes larger than 4GB are not supported'); - length = readUint32(reader); - } - - if (length <= 0 && skipEmpty) return undefined; - - let end = reader.offset + length; - if (end > reader.view.byteLength) throw new Error('Section exceeds file size'); - - const result = func(() => end - reader.offset); - - if (reader.offset !== end) { - if (reader.offset > end) { - warnOrThrow(reader, 'Exceeded section limits'); - } else { - warnOrThrow(reader, `Unread section data`); // : ${end - reader.offset} bytes at 0x${reader.offset.toString(16)}`); - } - } - - while (end % round) end++; - reader.offset = end; - - return result; +export async function readSection( + reader: PsdReader, + round: number, + func: (left: () => Promise) => Promise, + skipEmpty = true, + eightBytes = false +): Promise { + let length = readUint32(reader); + + if (eightBytes) { + if (length !== 0) + throw new Error("Sizes larger than 4GB are not supported"); + length = readUint32(reader); + } + + if (length <= 0 && skipEmpty) return undefined; + + let end = reader.offset + length; + if (end > reader.view.byteLength) + throw new Error("Section exceeds file size"); + + const result = await func(async () => end - reader.offset); + + if (reader.offset !== end) { + if (reader.offset > end) { + warnOrThrow(reader, "Exceeded section limits"); + } else { + warnOrThrow(reader, `Unread section data`); // : ${end - reader.offset} bytes at 0x${reader.offset.toString(16)}`); + } + } + + while (end % round) end++; + reader.offset = end; + + return result; } export function readColor(reader: PsdReader): Color { - const colorSpace = readUint16(reader) as ColorSpace; - - switch (colorSpace) { - case ColorSpace.RGB: { - const r = readUint16(reader) / 257; - const g = readUint16(reader) / 257; - const b = readUint16(reader) / 257; - skipBytes(reader, 2); - return { r, g, b }; - } - case ColorSpace.HSB: { - const h = readUint16(reader) / 0xffff; - const s = readUint16(reader) / 0xffff; - const b = readUint16(reader) / 0xffff; - skipBytes(reader, 2); - return { h, s, b }; - } - case ColorSpace.CMYK: { - const c = readUint16(reader) / 257; - const m = readUint16(reader) / 257; - const y = readUint16(reader) / 257; - const k = readUint16(reader) / 257; - return { c, m, y, k }; - } - case ColorSpace.Lab: { - const l = readInt16(reader) / 10000; - const ta = readInt16(reader); - const tb = readInt16(reader); - const a = ta < 0 ? (ta / 12800) : (ta / 12700); - const b = tb < 0 ? (tb / 12800) : (tb / 12700); - skipBytes(reader, 2); - return { l, a, b }; - } - case ColorSpace.Grayscale: { - const k = readUint16(reader) * 255 / 10000; - skipBytes(reader, 6); - return { k }; - } - default: - throw new Error('Invalid color space'); - } + const colorSpace = readUint16(reader) as ColorSpace; + + switch (colorSpace) { + case ColorSpace.RGB: { + const r = readUint16(reader) / 257; + const g = readUint16(reader) / 257; + const b = readUint16(reader) / 257; + skipBytes(reader, 2); + return { r, g, b }; + } + case ColorSpace.HSB: { + const h = readUint16(reader) / 0xffff; + const s = readUint16(reader) / 0xffff; + const b = readUint16(reader) / 0xffff; + skipBytes(reader, 2); + return { h, s, b }; + } + case ColorSpace.CMYK: { + const c = readUint16(reader) / 257; + const m = readUint16(reader) / 257; + const y = readUint16(reader) / 257; + const k = readUint16(reader) / 257; + return { c, m, y, k }; + } + case ColorSpace.Lab: { + const l = readInt16(reader) / 10000; + const ta = readInt16(reader); + const tb = readInt16(reader); + const a = ta < 0 ? ta / 12800 : ta / 12700; + const b = tb < 0 ? tb / 12800 : tb / 12700; + skipBytes(reader, 2); + return { l, a, b }; + } + case ColorSpace.Grayscale: { + const k = (readUint16(reader) * 255) / 10000; + skipBytes(reader, 6); + return { k }; + } + default: + throw new Error("Invalid color space"); + } } export function readPattern(reader: PsdReader): PatternInfo { - readUint32(reader); // length - const version = readUint32(reader); - if (version !== 1) throw new Error(`Invalid pattern version: ${version}`); - - const colorMode = readUint32(reader) as ColorMode; - const x = readInt16(reader); - const y = readInt16(reader); - - // we only support RGB and grayscale for now - if (colorMode !== ColorMode.RGB && colorMode !== ColorMode.Grayscale && colorMode !== ColorMode.Indexed) { - throw new Error(`Unsupported pattern color mode: ${colorMode}`); - } - - let name = readUnicodeString(reader); - const id = readPascalString(reader, 1); - const palette: RGB[] = []; - - if (colorMode === ColorMode.Indexed) { - for (let i = 0; i < 256; i++) { - palette.push({ - r: readUint8(reader), - g: readUint8(reader), - b: readUint8(reader), - }) - } - - skipBytes(reader, 4); // no idea what this is - } - - // virtual memory array list - const version2 = readUint32(reader); - if (version2 !== 3) throw new Error(`Invalid pattern VMAL version: ${version2}`); - - readUint32(reader); // length - const top = readUint32(reader); - const left = readUint32(reader); - const bottom = readUint32(reader); - const right = readUint32(reader); - const channelsCount = readUint32(reader); - const width = right - left; - const height = bottom - top; - const data = new Uint8Array(width * height * 4); - - for (let i = 3; i < data.byteLength; i += 4) { - data[i] = 255; - } - - for (let i = 0, ch = 0; i < (channelsCount + 2); i++) { - const has = readUint32(reader); - if (!has) continue; - - const length = readUint32(reader); - const pixelDepth = readUint32(reader); - const ctop = readUint32(reader); - const cleft = readUint32(reader); - const cbottom = readUint32(reader); - const cright = readUint32(reader); - const pixelDepth2 = readUint16(reader); - const compressionMode = readUint8(reader); // 0 - raw, 1 - zip - const dataLength = length - (4 + 16 + 2 + 1); - const cdata = readBytes(reader, dataLength); - - if (pixelDepth !== 8 || pixelDepth2 !== 8) { - throw new Error('16bit pixel depth not supported for patterns'); - } - - const w = cright - cleft; - const h = cbottom - ctop; - const ox = cleft - left; - const oy = ctop - top; - - if (compressionMode === 0) { - if (colorMode === ColorMode.RGB && ch < 3) { - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const src = x + y * w; - const dst = (ox + x + (y + oy) * width) * 4; - data[dst + ch] = cdata[src]; - } - } - } - - if (colorMode === ColorMode.Grayscale && ch < 1) { - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const src = x + y * w; - const dst = (ox + x + (y + oy) * width) * 4; - const value = cdata[src]; - data[dst + 0] = value; - data[dst + 1] = value; - data[dst + 2] = value; - } - } - } - - if (colorMode === ColorMode.Indexed) { - // TODO: - throw new Error('Indexed pattern color mode not implemented'); - } - } else if (compressionMode === 1) { - // console.log({ colorMode }); - // require('fs').writeFileSync('zip.bin', Buffer.from(cdata)); - // const data = require('zlib').inflateRawSync(cdata); - // const data = require('zlib').unzipSync(cdata); - // console.log(data); - // throw new Error('Zip compression not supported for pattern'); - // throw new Error('Unsupported pattern compression'); - console.error('Unsupported pattern compression'); - name += ' (failed to decode)'; - } else { - throw new Error('Invalid pattern compression mode'); - } - - ch++; - } - - // TODO: use canvas instead of data ? - - return { id, name, x, y, bounds: { x: left, y: top, w: width, h: height }, data }; + readUint32(reader); // length + const version = readUint32(reader); + if (version !== 1) throw new Error(`Invalid pattern version: ${version}`); + + const colorMode = readUint32(reader) as ColorMode; + const x = readInt16(reader); + const y = readInt16(reader); + + // we only support RGB and grayscale for now + if ( + colorMode !== ColorMode.RGB && + colorMode !== ColorMode.Grayscale && + colorMode !== ColorMode.Indexed + ) { + throw new Error(`Unsupported pattern color mode: ${colorMode}`); + } + + let name = readUnicodeString(reader); + const id = readPascalString(reader, 1); + const palette: RGB[] = []; + + if (colorMode === ColorMode.Indexed) { + for (let i = 0; i < 256; i++) { + palette.push({ + r: readUint8(reader), + g: readUint8(reader), + b: readUint8(reader), + }); + } + + skipBytes(reader, 4); // no idea what this is + } + + // virtual memory array list + const version2 = readUint32(reader); + if (version2 !== 3) + throw new Error(`Invalid pattern VMAL version: ${version2}`); + + readUint32(reader); // length + const top = readUint32(reader); + const left = readUint32(reader); + const bottom = readUint32(reader); + const right = readUint32(reader); + const channelsCount = readUint32(reader); + const width = right - left; + const height = bottom - top; + const data = new Uint8Array(width * height * 4); + + for (let i = 3; i < data.byteLength; i += 4) { + data[i] = 255; + } + + for (let i = 0, ch = 0; i < channelsCount + 2; i++) { + const has = readUint32(reader); + if (!has) continue; + + const length = readUint32(reader); + const pixelDepth = readUint32(reader); + const ctop = readUint32(reader); + const cleft = readUint32(reader); + const cbottom = readUint32(reader); + const cright = readUint32(reader); + const pixelDepth2 = readUint16(reader); + const compressionMode = readUint8(reader); // 0 - raw, 1 - zip + const dataLength = length - (4 + 16 + 2 + 1); + const cdata = readBytes(reader, dataLength); + + if (pixelDepth !== 8 || pixelDepth2 !== 8) { + throw new Error("16bit pixel depth not supported for patterns"); + } + + const w = cright - cleft; + const h = cbottom - ctop; + const ox = cleft - left; + const oy = ctop - top; + + if (compressionMode === 0) { + if (colorMode === ColorMode.RGB && ch < 3) { + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const src = x + y * w; + const dst = (ox + x + (y + oy) * width) * 4; + data[dst + ch] = cdata[src]; + } + } + } + + if (colorMode === ColorMode.Grayscale && ch < 1) { + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const src = x + y * w; + const dst = (ox + x + (y + oy) * width) * 4; + const value = cdata[src]; + data[dst + 0] = value; + data[dst + 1] = value; + data[dst + 2] = value; + } + } + } + + if (colorMode === ColorMode.Indexed) { + // TODO: + throw new Error("Indexed pattern color mode not implemented"); + } + } else if (compressionMode === 1) { + // console.log({ colorMode }); + // require('fs').writeFileSync('zip.bin', Buffer.from(cdata)); + // const data = require('zlib').inflateRawSync(cdata); + // const data = require('zlib').unzipSync(cdata); + // console.log(data); + // throw new Error('Zip compression not supported for pattern'); + // throw new Error('Unsupported pattern compression'); + console.error("Unsupported pattern compression"); + name += " (failed to decode)"; + } else { + throw new Error("Invalid pattern compression mode"); + } + + ch++; + } + + // TODO: use canvas instead of data ? + + return { + id, + name, + x, + y, + bounds: { x: left, y: top, w: width, h: height }, + data, + }; } diff --git a/src/test/common.ts b/src/test/common.ts index 412bc0c..80984dc 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,201 +2,253 @@ /// /// -require('source-map-support').install(); - -import * as fs from 'fs'; -import * as path from 'path'; -import { createCanvas, Image } from 'canvas'; -import '../initializeCanvas'; -import { Psd, ReadOptions } from '../index'; -import { readPsd, createReader } from '../psdReader'; -import { setLogErrors } from '../descriptor'; +require("source-map-support").install(); + +import { createCanvas, Image } from "canvas"; +import * as fs from "fs"; +import * as path from "path"; +import { setLogErrors } from "../descriptor"; +import { Psd } from "../index"; +import "../initializeCanvas"; +import { createReader } from "../psdReader"; export { createCanvas }; setLogErrors(true); -const resultsPath = path.join(__dirname, '..', '..', 'results'); +const resultsPath = path.join(__dirname, "..", "..", "results"); export type ImageMap = { [key: string]: HTMLCanvasElement }; export function toArrayBuffer(buffer: Buffer) { - const ab = new ArrayBuffer(buffer.length); - const view = new Uint8Array(ab); + const ab = new ArrayBuffer(buffer.length); + const view = new Uint8Array(ab); - for (let i = 0; i < buffer.length; ++i) { - view[i] = buffer[i]; - } + for (let i = 0; i < buffer.length; ++i) { + view[i] = buffer[i]; + } - return ab; + return ab; } export function repeat(times: number, ...values: T[]): T[] { - if (!values.length) { - throw new Error('missing values'); - } + if (!values.length) { + throw new Error("missing values"); + } - const array: T[] = []; + const array: T[] = []; - for (let i = 0; i < times; i++) { - array.push(...values); - } + for (let i = 0; i < times; i++) { + array.push(...values); + } - return array; + return array; } export function range(start: number, length: number): number[] { - const array: number[] = []; + const array: number[] = []; - for (let i = 0; i < length; i++) { - array.push(start + i); - } + for (let i = 0; i < length; i++) { + array.push(start + i); + } - return array; + return array; } export function importPSD(dirName: string): Psd | undefined { - const dataPath = path.join(dirName, 'data.json'); + const dataPath = path.join(dirName, "data.json"); - if (!fs.existsSync(dataPath)) - return undefined; + if (!fs.existsSync(dataPath)) return undefined; - return JSON.parse(fs.readFileSync(dataPath, 'utf8')); + return JSON.parse(fs.readFileSync(dataPath, "utf8")); } export function loadImagesFromDirectory(dirName: string) { - const images: ImageMap = {}; + const images: ImageMap = {}; - fs.readdirSync(dirName) - .filter(f => /\.png$/.test(f)) - .forEach(f => images[f] = loadCanvasFromFile(path.join(dirName, f))); + fs.readdirSync(dirName) + .filter((f) => /\.png$/.test(f)) + .forEach((f) => (images[f] = loadCanvasFromFile(path.join(dirName, f)))); - return images; + return images; } export function createReaderFromBuffer(buffer: Buffer) { - const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength); - // reader.strict = true; - reader.debug = true; // for testing - return reader; + const reader = createReader( + buffer.buffer, + buffer.byteOffset, + buffer.byteLength + ); + // reader.strict = true; + reader.debug = true; // for testing + return reader; } -export function readPsdFromFile(fileName: string, options?: ReadOptions): Psd { - const buffer = fs.readFileSync(fileName); - const reader = createReaderFromBuffer(buffer); - return readPsd(reader, options); -} +// export function readPsdFromFile(fileName: string, options?: ReadOptions): Psd { +// const buffer = fs.readFileSync(fileName); +// const reader = createReaderFromBuffer(buffer); +// return readPsd(reader, options); +// } export function extractPSD(filePath: string, psd: Psd) { - const basePath = path.join(resultsPath, filePath); - - if (!fs.existsSync(basePath)) - fs.mkdirSync(basePath); - - if (psd.canvas) { - fs.writeFileSync(path.join(basePath, 'canvas.png'), psd.canvas.toBuffer()); - psd.canvas = undefined; - } - - psd.children!.forEach((l, i) => { - if (l.canvas) { - fs.writeFileSync(path.join(basePath, `layer-${i}.png`), l.canvas.toBuffer()); - l.canvas = undefined; - } - }); - - fs.writeFileSync(path.join(basePath, 'data.json'), JSON.stringify(psd, null, 2)); + const basePath = path.join(resultsPath, filePath); + + if (!fs.existsSync(basePath)) fs.mkdirSync(basePath); + + if (psd.canvas) { + fs.writeFileSync(path.join(basePath, "canvas.png"), psd.canvas.toBuffer()); + psd.canvas = undefined; + } + + psd.children!.forEach((l, i) => { + if (l.canvas) { + fs.writeFileSync( + path.join(basePath, `layer-${i}.png`), + l.canvas.toBuffer() + ); + l.canvas = undefined; + } + }); + + fs.writeFileSync( + path.join(basePath, "data.json"), + JSON.stringify(psd, null, 2) + ); } -export function saveCanvas(fileName: string, canvas: HTMLCanvasElement | undefined) { - if (canvas) { - fs.writeFileSync(fileName, canvas.toBuffer()); - } +export function saveCanvas( + fileName: string, + canvas: HTMLCanvasElement | undefined +) { + if (canvas) { + fs.writeFileSync(fileName, canvas.toBuffer()); + } } export function loadCanvasFromFile(filePath: string) { - const img = new Image(); - img.src = fs.readFileSync(filePath); - const canvas = createCanvas(img.width, img.height); - canvas.getContext('2d')!.drawImage(img, 0, 0); - return canvas; + const img = new Image(); + img.src = fs.readFileSync(filePath); + const canvas = createCanvas(img.width, img.height); + canvas.getContext("2d")!.drawImage(img, 0, 0); + return canvas; } -export function compareTwoFiles(expectedPath: string, actual: Uint8Array, name: string) { - const expectedBuffer = fs.readFileSync(expectedPath); - const expected = new Uint8Array(expectedBuffer.buffer, expectedBuffer.byteOffset, expectedBuffer.byteLength); - - if (expected.byteLength !== actual.byteLength) { - throw new Error(`File size is different than expected (${name})`); - } - - for (let i = 0; i < expected.byteLength; i++) { - if (expected[i] !== actual[i]) { - throw new Error(`Actual file different than expected at index ${i}: actual ${actual[i]}, expected ${expected[i]}`); - } - } +export function compareTwoFiles( + expectedPath: string, + actual: Uint8Array, + name: string +) { + const expectedBuffer = fs.readFileSync(expectedPath); + const expected = new Uint8Array( + expectedBuffer.buffer, + expectedBuffer.byteOffset, + expectedBuffer.byteLength + ); + + if (expected.byteLength !== actual.byteLength) { + throw new Error(`File size is different than expected (${name})`); + } + + for (let i = 0; i < expected.byteLength; i++) { + if (expected[i] !== actual[i]) { + throw new Error( + `Actual file different than expected at index ${i}: actual ${actual[i]}, expected ${expected[i]}` + ); + } + } } -export function compareCanvases(expected: HTMLCanvasElement | undefined, actual: HTMLCanvasElement | undefined, name: string) { - const saveFailure = () => { - const failuresDir = path.join(resultsPath, 'failures'); - if (!fs.existsSync(failuresDir)) { - fs.mkdirSync(failuresDir); - } - fs.writeFileSync(path.join(failuresDir, `${name.replace(/[\\/]/, '-')}`), actual!.toBuffer()); - }; - - if (expected === actual) return; - if (!expected) throw new Error(`Expected canvas is null (${name})`); - if (!actual) throw new Error(`Actual canvas is null (${name})`); - - if (expected.width !== actual.width || expected.height !== actual.height) { - saveFailure(); - throw new Error(`Canvas size is different than expected (${name})`); - } - - const expectedData = expected.getContext('2d')!.getImageData(0, 0, expected.width, expected.height); - const actualData = actual.getContext('2d')!.getImageData(0, 0, actual.width, actual.height); - const length = expectedData.width * expectedData.height * 4; - - for (let i = 0; i < length; i++) { - if (expectedData.data[i] !== actualData.data[i]) { - saveFailure(); - const expectedNumBytes = expectedData.data.length; - const actualNumBytes = actualData.data.length; - throw new Error( - `Actual canvas (${actualNumBytes} bytes) different than ` + - `expected (${name}) (${expectedNumBytes} bytes) ` + - `at index ${i}: actual ${actualData.data[i]} vs. expected ${expectedData.data[i]}` - ); - } - } +export function compareCanvases( + expected: HTMLCanvasElement | undefined, + actual: HTMLCanvasElement | undefined, + name: string +) { + const saveFailure = () => { + const failuresDir = path.join(resultsPath, "failures"); + if (!fs.existsSync(failuresDir)) { + fs.mkdirSync(failuresDir); + } + fs.writeFileSync( + path.join(failuresDir, `${name.replace(/[\\/]/, "-")}`), + actual!.toBuffer() + ); + }; + + if (expected === actual) return; + if (!expected) throw new Error(`Expected canvas is null (${name})`); + if (!actual) throw new Error(`Actual canvas is null (${name})`); + + if (expected.width !== actual.width || expected.height !== actual.height) { + saveFailure(); + throw new Error(`Canvas size is different than expected (${name})`); + } + + const expectedData = expected + .getContext("2d")! + .getImageData(0, 0, expected.width, expected.height); + const actualData = actual + .getContext("2d")! + .getImageData(0, 0, actual.width, actual.height); + const length = expectedData.width * expectedData.height * 4; + + for (let i = 0; i < length; i++) { + if (expectedData.data[i] !== actualData.data[i]) { + saveFailure(); + const expectedNumBytes = expectedData.data.length; + const actualNumBytes = actualData.data.length; + throw new Error( + `Actual canvas (${actualNumBytes} bytes) different than ` + + `expected (${name}) (${expectedNumBytes} bytes) ` + + `at index ${i}: actual ${actualData.data[i]} vs. expected ${expectedData.data[i]}` + ); + } + } } -export function compareBuffers(actual: Buffer, expected: Buffer, test: string, start = 0, offset = 0) { - if (!actual) - throw new Error(`Actual buffer is null or undefined (${test})`); - if (!expected) - throw new Error(`Expected buffer is null or undefined (${test})`); - - for (let i = start; i < expected.length; i++) { - if (expected[i] !== actual[i + offset]) { - throw new Error(`Buffers differ ` + - `expected: 0x${expected[i]?.toString(16)} at [0x${i?.toString(16)}] ` + - `actual: 0x${actual[i + offset]?.toString(16)} at [0x${(i + offset)?.toString(16)}] (${test})`); - } - } - - if (actual.length !== expected.length) - throw new Error(`Buffers differ in size actual: ${actual.length} expected: ${expected.length} (${test})`); +export function compareBuffers( + actual: Buffer, + expected: Buffer, + test: string, + start = 0, + offset = 0 +) { + if (!actual) throw new Error(`Actual buffer is null or undefined (${test})`); + if (!expected) + throw new Error(`Expected buffer is null or undefined (${test})`); + + for (let i = start; i < expected.length; i++) { + if (expected[i] !== actual[i + offset]) { + throw new Error( + `Buffers differ ` + + `expected: 0x${expected[i]?.toString(16)} at [0x${i?.toString( + 16 + )}] ` + + `actual: 0x${actual[i + offset]?.toString(16)} at [0x${( + i + offset + )?.toString(16)}] (${test})` + ); + } + } + + if (actual.length !== expected.length) + throw new Error( + `Buffers differ in size actual: ${actual.length} expected: ${expected.length} (${test})` + ); } -export function expectBuffersEqual(actual: Uint8Array, expected: Uint8Array, name: string) { - const length = Math.max(actual.length, expected.length); - - for (let i = 0; i < length; i++) { - if (actual[i] !== expected[i]) { - fs.writeFileSync(path.join(__dirname, '..', '..', 'results', name), Buffer.from(actual)); - throw new Error(`Different byte at 0x${i.toString(16)} in (${name})`); - } - } +export function expectBuffersEqual( + actual: Uint8Array, + expected: Uint8Array, + name: string +) { + const length = Math.max(actual.length, expected.length); + + for (let i = 0; i < length; i++) { + if (actual[i] !== expected[i]) { + fs.writeFileSync( + path.join(__dirname, "..", "..", "results", name), + Buffer.from(actual) + ); + throw new Error(`Different byte at 0x${i.toString(16)} in (${name})`); + } + } } diff --git a/src/test/psdReader.spec.ts b/src/test/psdReader.spec.ts index 965bb06..1f221df 100644 --- a/src/test/psdReader.spec.ts +++ b/src/test/psdReader.spec.ts @@ -1,474 +1,474 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { expect } from 'chai'; -import { readPsdFromFile, importPSD, loadImagesFromDirectory, compareCanvases, saveCanvas, createReaderFromBuffer, compareBuffers, compareTwoFiles } from './common'; -import { Layer, ReadOptions, Psd } from '../psd'; -import { byteArrayToBase64, readPsd, writePsdBuffer } from '../index'; -import { readPsd as readPsdInternal } from '../psdReader'; -import { decodeEngineData2 } from '../engineData2'; - -const testFilesPath = path.join(__dirname, '..', '..', 'test'); -const readFilesPath = path.join(testFilesPath, 'read'); -const readWriteFilesPath = path.join(testFilesPath, 'read-write'); -const resultsFilesPath = path.join(__dirname, '..', '..', 'results'); -const opts: ReadOptions = { - throwForMissingFeatures: true, - logMissingFeatures: true, -}; - -describe('PsdReader', () => { - it('reads width and height properly', () => { - const psd = readPsdFromFile(path.join(readFilesPath, 'blend-mode', 'src.psd'), { ...opts }); - expect(psd.width).equal(300); - expect(psd.height).equal(200); - }); - - it('skips composite image data', () => { - const psd = readPsdFromFile(path.join(readFilesPath, 'layers', 'src.psd'), { ...opts, skipCompositeImageData: true }); - expect(psd.canvas).not.ok; - }); - - it('skips layer image data', () => { - const psd = readPsdFromFile(path.join(readFilesPath, 'layers', 'src.psd'), { ...opts, skipLayerImageData: true }); - expect(psd.children![0].canvas).not.ok; - }); - - it('reads PSD from Buffer with offset', () => { - const file = fs.readFileSync(path.join(readFilesPath, 'layers', 'src.psd')); - const outer = Buffer.alloc(file.byteLength + 100); - file.copy(outer, 100); - const inner = Buffer.from(outer.buffer, 100, file.byteLength); - - const psd = readPsd(inner, opts); - - expect(psd.width).equal(300); - }); - - it.skip('duplicate smart', () => { - const psd = readPsdFromFile(path.join('resources', 'src.psd'), { ...opts }); - - const child = psd.children![1].children![0]; - psd.children![1].children!.push(child); - - // const child = psd.children![0]; - // delete child.id; - // psd.children!.push(child); - - fs.writeFileSync('output.psd', writePsdBuffer(psd, { - trimImageData: false, - generateThumbnail: true, - noBackground: true - })); - - const psd2 = readPsdFromFile(path.join('output.psd'), { ...opts }); - - console.log(psd2.width); - }); - - // skipping "pattern" test because it requires zip cimpression of patterns - // skipping "cmyk" test because we can't convert CMYK to RGB - fs.readdirSync(readFilesPath).filter(f => !/pattern|cmyk/.test(f)).forEach(f => { - // fs.readdirSync(readFilesPath).filter(f => /ignore-smart-filter/.test(f)).forEach(f => { - it(`reads PSD file (${f})`, () => { - const basePath = path.join(readFilesPath, f); - const fileName = fs.existsSync(path.join(basePath, 'src.psb')) ? 'src.psb' : 'src.psd'; - const psd = readPsdFromFile(path.join(basePath, fileName), { - ...opts, - // logDevFeatures: true, - }); - const expected = importPSD(basePath); - const images = loadImagesFromDirectory(basePath); - const compare: { name: string; canvas: HTMLCanvasElement | undefined; skip?: boolean; }[] = []; - const compareFiles: { name: string; data: Uint8Array; }[] = []; - - compare.push({ name: `canvas.png`, canvas: psd.canvas }); - psd.canvas = undefined; - delete psd.imageData; - if (psd.imageResources) delete psd.imageResources.xmpMetadata; - - let i = 0; - - function pushLayerCanvases(layers: Layer[]) { - for (const l of layers) { - const layerId = i; - - if (!l.children || l.mask) i++; - - if (l.children) { - pushLayerCanvases(l.children); - } else { - compare.push({ name: `layer-${layerId}.png`, canvas: l.canvas }); - l.canvas = undefined; - delete l.imageData; - } - - if (l.mask) { - compare.push({ name: `layer-${layerId}-mask.png`, canvas: l.mask.canvas }); - delete l.mask.canvas; - delete l.mask.imageData; - } - - // if (l.vectorMask) { - // const canvas = createCanvas(l.right! - l.left!, l.bottom! - l.top!); - // const context = canvas.getContext('2d')!; - // context.translate(-l.left!, -l.top!); - // const knots = l.vectorMask.paths[0].knots; - // context.beginPath(); - // context.moveTo(knots[knots.length - 1].points[2], knots[knots.length - 1].points[3]); - // for (let i = 0; i < knots.length; i++) { - // const prev = i ? knots[i - 1].points : knots[knots.length - 1].points; - // const points = knots[i].points; - // context.bezierCurveTo(prev[4], prev[5], points[0], points[1], points[2], points[3]); - // } - // context.closePath(); - // context.fill(); - // fs.writeFileSync('test.png', canvas.toBuffer()); - // } - } - } - - function convertUint8ArraysToBase64(layers: Layer[]) { - for (const layer of layers) { - if (layer.adjustment?.type == 'color lookup') { - if (layer.adjustment.lut3DFileData) { - layer.adjustment.lut3DFileData = byteArrayToBase64(layer.adjustment.lut3DFileData) as any; - } - - if (layer.adjustment.profile) { - layer.adjustment.profile = byteArrayToBase64(layer.adjustment.profile) as any; - } - } - - if (layer.children) { - convertUint8ArraysToBase64(layer.children); - } - - const item = layer.placedLayer?.filter?.list[0]; - if (item && item.type === 'liquify') { - item.filter.liquifyMesh = byteArrayToBase64(item.filter.liquifyMesh) as any; - } - } - } - - if (psd.linkedFiles) { - for (const file of psd.linkedFiles) { - if (file.data) { - compareFiles.push({ name: file.name, data: file.data }); - delete file.data; - } - } - } - - if (psd.filterEffectsMasks) { - for (const mask of psd.filterEffectsMasks) { - for (let i = 0; i < mask.channels.length; i++) { - if (mask.channels[i]) { - mask.channels[i]!.data = byteArrayToBase64(mask.channels[i]!.data) as any; - } else { - mask.channels[i] = null as any; - } - } - - if (mask.extra?.data) { - mask.extra!.data = byteArrayToBase64(mask.extra!.data) as any; - } - } - } - - pushLayerCanvases(psd.children || []); - convertUint8ArraysToBase64(psd.children || []); - - const resultsDir = path.join(resultsFilesPath, 'read', f); - fs.mkdirSync(resultsDir, { recursive: true }); - - if (psd.imageResources?.thumbnail) { - compare.push({ name: 'thumb.png', canvas: psd.imageResources.thumbnail, skip: true }); - delete psd.imageResources.thumbnail; - } - - if (psd.imageResources) delete psd.imageResources.thumbnailRaw; - - compare.forEach(i => saveCanvas(path.join(resultsDir, i.name), i.canvas)); - compareFiles.forEach(i => fs.writeFileSync(path.join(resultsDir, i.name), i.data)); - - fs.writeFileSync(path.join(resultsDir, 'data.json'), JSON.stringify(psd, null, 2), 'utf8'); - - clearEmptyCanvasFields(psd); - clearEmptyCanvasFields(expected); - - expect(psd).eql(expected, f); - - compare.forEach(i => i.skip || compareCanvases(images[i.name], i.canvas, `${f}/${i.name}`)); - compareFiles.forEach(i => compareTwoFiles(path.join(basePath, i.name), i.data, `${f}/${i.name}`)); - }); - }); - - fs.readdirSync(readWriteFilesPath).forEach(f => { - // fs.readdirSync(readWriteFilesPath).filter(f => /text$/.test(f)).forEach(f => { - it(`reads-writes PSD file (${f})`, () => { - const ext = fs.existsSync(path.join(readWriteFilesPath, f, 'src.psb')) ? 'psb' : 'psd'; - const psd = readPsdFromFile(path.join(readWriteFilesPath, f, `src.${ext}`), { - ...opts, useImageData: true, useRawThumbnail: true, throwForMissingFeatures: true, - // skipCompositeImageData: true, skipLayerImageData: true, skipThumbnail: true, - // logDevFeatures: true, logMissingFeatures: true, - }); - - // console.log(psd.children![0].text); - // psd.children![0].text!.text = 'f'; - // psd.children![0].text!.style!.font!.name = 'ArialMT'; - // const actual = writePsdBuffer(psd, { logMissingFeatures: true, psb: ext === 'psb', invalidateTextLayers: true }); - - const actual = writePsdBuffer(psd, { logMissingFeatures: true, psb: ext === 'psb' }); - const resultsDir = path.join(resultsFilesPath, 'read-write', f); - fs.mkdirSync(resultsDir, { recursive: true }); - fs.writeFileSync(path.join(resultsDir, `expected.${ext}`), actual); - fs.writeFileSync(path.join(resultsDir, `expected.bin`), actual); - // console.log(require('util').inspect(psd, false, 99, true)); - - // const psd2 = readPsdFromFile(path.join(resultsDir, `raw.psd`), { ...opts, useImageData: true, useRawThumbnail: true }); - // fs.writeFileSync('temp.txt', require('util').inspect(psd, false, 99, false), 'utf8'); - // fs.writeFileSync('temp2.txt', require('util').inspect(psd2, false, 99, false), 'utf8'); - - const expected = fs.readFileSync(path.join(readWriteFilesPath, f, `expected.${ext}`)); - compareBuffers(actual, expected, `read-write-${f}`, 0x0); - }); - }); - - it.skip('generate file', () => { - fs.writeFileSync('test.psd', writePsdBuffer({ - width: 100, - height: 100, - children: [ - { - name: 'test', - blendMode: 'color burn', - blendClippendElements: true, - // blendInteriorElements: false, - }, - ] - })); - }); - - it.skip('write text layer test', () => { - const psd: Psd = { - width: 200, - height: 200, - children: [ - { - name: 'text layer', - text: { - text: 'Hello World\n• c • tiny!\r\ntest', - // orientation: 'vertical', - transform: [1, 0, 0, 1, 70, 70], - style: { - font: { name: 'ArialMT' }, - fontSize: 30, - fillColor: { r: 0, g: 128, b: 0 }, - }, - styleRuns: [ - { length: 12, style: { fillColor: { r: 255, g: 0, b: 0 } } }, - { length: 12, style: { fillColor: { r: 0, g: 0, b: 255 } } }, - { length: 4, style: { underline: true } }, - ], - paragraphStyle: { - justification: 'center', - }, - warp: { - style: 'arc', - value: 50, - perspective: 0, - perspectiveOther: 0, - rotate: 'horizontal', - }, - }, - }, - { - name: '2nd layer', - text: { - text: 'Aaaaa', - transform: [1, 0, 0, 1, 70, 70], - }, - }, - ], - }; - - fs.writeFileSync(path.join(resultsFilesPath, '_TEXT2.psd'), writePsdBuffer(psd, { logMissingFeatures: true })); - }); - - it.skip('read text layer test', () => { - const psd = readPsdFromFile(path.join(testFilesPath, 'text-test.psd'), opts); - // const layer = psd.children![1]; - - // layer.text!.text = 'Foo bar'; - const buffer = writePsdBuffer(psd, { logMissingFeatures: true }); - fs.writeFileSync(path.join(resultsFilesPath, '_TEXT.psd'), buffer); - - // console.log(require('util').inspect(psd.children![0].text, false, 99, true)); - // console.log(require('util').inspect(psd.children![1].text, false, 99, true)); - // console.log(require('util').inspect(psd.engineData, false, 99, true)); - }); - - it.skip('READ TEST', () => { - const originalBuffer = fs.readFileSync(path.join(testFilesPath, 'test.psd')); - - console.log('READING ORIGINAL'); - const opts = { - logMissingFeatures: true, - throwForMissingFeatures: true, - useImageData: true, - useRawThumbnail: true, - logDevFeatures: true, - }; - const originalPsd = readPsdInternal(createReaderFromBuffer(originalBuffer), opts); - - console.log('WRITING'); - const buffer = writePsdBuffer(originalPsd, { logMissingFeatures: true }); - fs.writeFileSync('temp.psd', buffer); - // fs.writeFileSync('temp.bin', buffer); - // fs.writeFileSync('temp.json', JSON.stringify(originalPsd, null, 2), 'utf8'); - // fs.writeFileSync('temp.xml', originalPsd.imageResources?.xmpMetadata, 'utf8'); - - console.log('READING WRITTEN'); - const psd = readPsdInternal( - createReaderFromBuffer(buffer), { logMissingFeatures: true, throwForMissingFeatures: true }); - - clearCanvasFields(originalPsd); - clearCanvasFields(psd); - delete originalPsd.imageResources!.thumbnail; - delete psd.imageResources!.thumbnail; - delete originalPsd.imageResources!.thumbnailRaw; - delete psd.imageResources!.thumbnailRaw; - // console.log(require('util').inspect(originalPsd, false, 99, true)); - - // fs.writeFileSync('original.json', JSON.stringify(originalPsd, null, 2)); - // fs.writeFileSync('after.json', JSON.stringify(psd, null, 2)); - - compareBuffers(buffer, originalBuffer, 'test'); - - expect(psd).eql(originalPsd); - }); - - it.skip('decode engine data 2', () => { - // const fileData = fs.readFileSync(path.join(__dirname, '..', '..', 'resources', 'engineData2Vertical.txt')); - const fileData = fs.readFileSync(path.join(__dirname, '..', '..', 'resources', 'engineData2Simple.txt')); - const func = new Function(`return ${fileData};`); - const data = func(); - const result = decodeEngineData2(data); - fs.writeFileSync( - path.join(__dirname, '..', '..', 'resources', 'temp.js'), - 'var x = ' + require('util').inspect(result, false, 99, false), 'utf8'); - }); - - it.skip('test.psd', () => { - const buffer = fs.readFileSync('test.psd'); - const psd = readPsdInternal(createReaderFromBuffer(buffer), { - skipCompositeImageData: true, - skipLayerImageData: true, - skipThumbnail: true, - throwForMissingFeatures: true, - logDevFeatures: true, - }); - delete psd.engineData; - psd.imageResources = {}; - console.log(require('util').inspect(psd, false, 99, true)); - }); - - it.skip('test', () => { - const psd = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`test/read-write/text-box/src.psd`)), { - // skipCompositeImageData: true, - // skipLayerImageData: true, - // skipThumbnail: true, - throwForMissingFeatures: true, - logDevFeatures: true, - useRawThumbnail: true, - }); - fs.writeFileSync('text_rect_out.psd', writePsdBuffer(psd, { logMissingFeatures: true })); - fs.writeFileSync('text_rect_out.bin', writePsdBuffer(psd, { logMissingFeatures: true })); - // const psd2 = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`text_rect_out.psd`)), { - // // skipCompositeImageData: true, - // // skipLayerImageData: true, - // // skipThumbnail: true, - // throwForMissingFeatures: true, - // logDevFeatures: true, - // }); - // psd2; - const original = fs.readFileSync(`test/read-write/text-box/src.psd`); - const output = fs.readFileSync(`text_rect_out.psd`); - compareBuffers(output, original, '-', 0x65d8); // , 0x8ce8, 0x8fca - 0x8ce8); - }); - - it.skip('compare test', () => { - for (const name of ['text_point', 'text_rect']) { - const psd = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`${name}.psd`)), { - skipCompositeImageData: true, - skipLayerImageData: true, - skipThumbnail: true, - throwForMissingFeatures: true, - logDevFeatures: true, - }); - // psd.imageResources = {}; - fs.writeFileSync(`${name}.txt`, require('util').inspect(psd, false, 99, false), 'utf8'); - - // const engineData = parseEngineData(toByteArray(psd.engineData!)); - // fs.writeFileSync(`${name}_enginedata.txt`, require('util').inspect(engineData, false, 99, false), 'utf8'); - } - }); - - it.skip('text-replace.psd', () => { - { - const buffer = fs.readFileSync('text-replace2.psd'); - const psd = readPsdInternal(createReaderFromBuffer(buffer), {}); - psd.children![1]!.text!.text = 'Foo bar'; - const output = writePsdBuffer(psd, { invalidateTextLayers: true, logMissingFeatures: true }); - fs.writeFileSync('out.psd', output); - } - - { - const buffer = fs.readFileSync('text-replace.psd'); - const psd = readPsdInternal(createReaderFromBuffer(buffer), { - skipCompositeImageData: true, - skipLayerImageData: true, - skipThumbnail: true, - throwForMissingFeatures: true, - logDevFeatures: true, - }); - delete psd.engineData; - psd.imageResources = {}; - psd.children?.splice(0, 1); - fs.writeFileSync('input.txt', require('util').inspect(psd, false, 99, false), 'utf8'); - } - - { - const buffer = fs.readFileSync('out.psd'); - const psd = readPsdInternal(createReaderFromBuffer(buffer), { - skipCompositeImageData: true, - skipLayerImageData: true, - skipThumbnail: true, - throwForMissingFeatures: true, - logDevFeatures: true, - }); - delete psd.engineData; - psd.imageResources = {}; - psd.children?.splice(0, 1); - fs.writeFileSync('output.txt', require('util').inspect(psd, false, 99, false), 'utf8'); - } - }); -}); - -function clearEmptyCanvasFields(layer: Layer | undefined) { - if (layer) { - if ('canvas' in layer && !layer.canvas) delete layer.canvas; - if ('imageData' in layer && !layer.imageData) delete layer.imageData; - layer.children?.forEach(clearEmptyCanvasFields); - } -} - -function clearCanvasFields(layer: Layer | undefined) { - if (layer) { - delete layer.canvas; - delete layer.imageData; - if (layer.mask) delete layer.mask.canvas; - if (layer.mask) delete layer.mask.imageData; - layer.children?.forEach(clearCanvasFields); - } -} +// import * as fs from 'fs'; +// import * as path from 'path'; +// import { expect } from 'chai'; +// import { readPsdFromFile, importPSD, loadImagesFromDirectory, compareCanvases, saveCanvas, createReaderFromBuffer, compareBuffers, compareTwoFiles } from './common'; +// import { Layer, ReadOptions, Psd } from '../psd'; +// import { byteArrayToBase64, readPsd, writePsdBuffer } from '../index'; +// import { readPsd as readPsdInternal } from '../psdReader'; +// import { decodeEngineData2 } from '../engineData2'; + +// const testFilesPath = path.join(__dirname, '..', '..', 'test'); +// const readFilesPath = path.join(testFilesPath, 'read'); +// const readWriteFilesPath = path.join(testFilesPath, 'read-write'); +// const resultsFilesPath = path.join(__dirname, '..', '..', 'results'); +// const opts: ReadOptions = { +// throwForMissingFeatures: true, +// logMissingFeatures: true, +// }; + +// describe('PsdReader', () => { +// it('reads width and height properly', () => { +// const psd = readPsdFromFile(path.join(readFilesPath, 'blend-mode', 'src.psd'), { ...opts }); +// expect(psd.width).equal(300); +// expect(psd.height).equal(200); +// }); + +// it('skips composite image data', () => { +// const psd = readPsdFromFile(path.join(readFilesPath, 'layers', 'src.psd'), { ...opts, skipCompositeImageData: true }); +// expect(psd.canvas).not.ok; +// }); + +// it('skips layer image data', () => { +// const psd = readPsdFromFile(path.join(readFilesPath, 'layers', 'src.psd'), { ...opts, skipLayerImageData: true }); +// expect(psd.children![0].canvas).not.ok; +// }); + +// it('reads PSD from Buffer with offset', () => { +// const file = fs.readFileSync(path.join(readFilesPath, 'layers', 'src.psd')); +// const outer = Buffer.alloc(file.byteLength + 100); +// file.copy(outer, 100); +// const inner = Buffer.from(outer.buffer, 100, file.byteLength); + +// const psd = readPsd(inner, opts); + +// expect(psd.width).equal(300); +// }); + +// it.skip('duplicate smart', () => { +// const psd = readPsdFromFile(path.join('resources', 'src.psd'), { ...opts }); + +// const child = psd.children![1].children![0]; +// psd.children![1].children!.push(child); + +// // const child = psd.children![0]; +// // delete child.id; +// // psd.children!.push(child); + +// fs.writeFileSync('output.psd', writePsdBuffer(psd, { +// trimImageData: false, +// generateThumbnail: true, +// noBackground: true +// })); + +// const psd2 = readPsdFromFile(path.join('output.psd'), { ...opts }); + +// console.log(psd2.width); +// }); + +// // skipping "pattern" test because it requires zip cimpression of patterns +// // skipping "cmyk" test because we can't convert CMYK to RGB +// fs.readdirSync(readFilesPath).filter(f => !/pattern|cmyk/.test(f)).forEach(f => { +// // fs.readdirSync(readFilesPath).filter(f => /ignore-smart-filter/.test(f)).forEach(f => { +// it(`reads PSD file (${f})`, () => { +// const basePath = path.join(readFilesPath, f); +// const fileName = fs.existsSync(path.join(basePath, 'src.psb')) ? 'src.psb' : 'src.psd'; +// const psd = readPsdFromFile(path.join(basePath, fileName), { +// ...opts, +// // logDevFeatures: true, +// }); +// const expected = importPSD(basePath); +// const images = loadImagesFromDirectory(basePath); +// const compare: { name: string; canvas: HTMLCanvasElement | undefined; skip?: boolean; }[] = []; +// const compareFiles: { name: string; data: Uint8Array; }[] = []; + +// compare.push({ name: `canvas.png`, canvas: psd.canvas }); +// psd.canvas = undefined; +// delete psd.imageData; +// if (psd.imageResources) delete psd.imageResources.xmpMetadata; + +// let i = 0; + +// function pushLayerCanvases(layers: Layer[]) { +// for (const l of layers) { +// const layerId = i; + +// if (!l.children || l.mask) i++; + +// if (l.children) { +// pushLayerCanvases(l.children); +// } else { +// compare.push({ name: `layer-${layerId}.png`, canvas: l.canvas }); +// l.canvas = undefined; +// delete l.imageData; +// } + +// if (l.mask) { +// compare.push({ name: `layer-${layerId}-mask.png`, canvas: l.mask.canvas }); +// delete l.mask.canvas; +// delete l.mask.imageData; +// } + +// // if (l.vectorMask) { +// // const canvas = createCanvas(l.right! - l.left!, l.bottom! - l.top!); +// // const context = canvas.getContext('2d')!; +// // context.translate(-l.left!, -l.top!); +// // const knots = l.vectorMask.paths[0].knots; +// // context.beginPath(); +// // context.moveTo(knots[knots.length - 1].points[2], knots[knots.length - 1].points[3]); +// // for (let i = 0; i < knots.length; i++) { +// // const prev = i ? knots[i - 1].points : knots[knots.length - 1].points; +// // const points = knots[i].points; +// // context.bezierCurveTo(prev[4], prev[5], points[0], points[1], points[2], points[3]); +// // } +// // context.closePath(); +// // context.fill(); +// // fs.writeFileSync('test.png', canvas.toBuffer()); +// // } +// } +// } + +// function convertUint8ArraysToBase64(layers: Layer[]) { +// for (const layer of layers) { +// if (layer.adjustment?.type == 'color lookup') { +// if (layer.adjustment.lut3DFileData) { +// layer.adjustment.lut3DFileData = byteArrayToBase64(layer.adjustment.lut3DFileData) as any; +// } + +// if (layer.adjustment.profile) { +// layer.adjustment.profile = byteArrayToBase64(layer.adjustment.profile) as any; +// } +// } + +// if (layer.children) { +// convertUint8ArraysToBase64(layer.children); +// } + +// const item = layer.placedLayer?.filter?.list[0]; +// if (item && item.type === 'liquify') { +// item.filter.liquifyMesh = byteArrayToBase64(item.filter.liquifyMesh) as any; +// } +// } +// } + +// if (psd.linkedFiles) { +// for (const file of psd.linkedFiles) { +// if (file.data) { +// compareFiles.push({ name: file.name, data: file.data }); +// delete file.data; +// } +// } +// } + +// if (psd.filterEffectsMasks) { +// for (const mask of psd.filterEffectsMasks) { +// for (let i = 0; i < mask.channels.length; i++) { +// if (mask.channels[i]) { +// mask.channels[i]!.data = byteArrayToBase64(mask.channels[i]!.data) as any; +// } else { +// mask.channels[i] = null as any; +// } +// } + +// if (mask.extra?.data) { +// mask.extra!.data = byteArrayToBase64(mask.extra!.data) as any; +// } +// } +// } + +// pushLayerCanvases(psd.children || []); +// convertUint8ArraysToBase64(psd.children || []); + +// const resultsDir = path.join(resultsFilesPath, 'read', f); +// fs.mkdirSync(resultsDir, { recursive: true }); + +// if (psd.imageResources?.thumbnail) { +// compare.push({ name: 'thumb.png', canvas: psd.imageResources.thumbnail, skip: true }); +// delete psd.imageResources.thumbnail; +// } + +// if (psd.imageResources) delete psd.imageResources.thumbnailRaw; + +// compare.forEach(i => saveCanvas(path.join(resultsDir, i.name), i.canvas)); +// compareFiles.forEach(i => fs.writeFileSync(path.join(resultsDir, i.name), i.data)); + +// fs.writeFileSync(path.join(resultsDir, 'data.json'), JSON.stringify(psd, null, 2), 'utf8'); + +// clearEmptyCanvasFields(psd); +// clearEmptyCanvasFields(expected); + +// expect(psd).eql(expected, f); + +// compare.forEach(i => i.skip || compareCanvases(images[i.name], i.canvas, `${f}/${i.name}`)); +// compareFiles.forEach(i => compareTwoFiles(path.join(basePath, i.name), i.data, `${f}/${i.name}`)); +// }); +// }); + +// fs.readdirSync(readWriteFilesPath).forEach(f => { +// // fs.readdirSync(readWriteFilesPath).filter(f => /text$/.test(f)).forEach(f => { +// it(`reads-writes PSD file (${f})`, () => { +// const ext = fs.existsSync(path.join(readWriteFilesPath, f, 'src.psb')) ? 'psb' : 'psd'; +// const psd = readPsdFromFile(path.join(readWriteFilesPath, f, `src.${ext}`), { +// ...opts, useImageData: true, useRawThumbnail: true, throwForMissingFeatures: true, +// // skipCompositeImageData: true, skipLayerImageData: true, skipThumbnail: true, +// // logDevFeatures: true, logMissingFeatures: true, +// }); + +// // console.log(psd.children![0].text); +// // psd.children![0].text!.text = 'f'; +// // psd.children![0].text!.style!.font!.name = 'ArialMT'; +// // const actual = writePsdBuffer(psd, { logMissingFeatures: true, psb: ext === 'psb', invalidateTextLayers: true }); + +// const actual = writePsdBuffer(psd, { logMissingFeatures: true, psb: ext === 'psb' }); +// const resultsDir = path.join(resultsFilesPath, 'read-write', f); +// fs.mkdirSync(resultsDir, { recursive: true }); +// fs.writeFileSync(path.join(resultsDir, `expected.${ext}`), actual); +// fs.writeFileSync(path.join(resultsDir, `expected.bin`), actual); +// // console.log(require('util').inspect(psd, false, 99, true)); + +// // const psd2 = readPsdFromFile(path.join(resultsDir, `raw.psd`), { ...opts, useImageData: true, useRawThumbnail: true }); +// // fs.writeFileSync('temp.txt', require('util').inspect(psd, false, 99, false), 'utf8'); +// // fs.writeFileSync('temp2.txt', require('util').inspect(psd2, false, 99, false), 'utf8'); + +// const expected = fs.readFileSync(path.join(readWriteFilesPath, f, `expected.${ext}`)); +// compareBuffers(actual, expected, `read-write-${f}`, 0x0); +// }); +// }); + +// it.skip('generate file', () => { +// fs.writeFileSync('test.psd', writePsdBuffer({ +// width: 100, +// height: 100, +// children: [ +// { +// name: 'test', +// blendMode: 'color burn', +// blendClippendElements: true, +// // blendInteriorElements: false, +// }, +// ] +// })); +// }); + +// it.skip('write text layer test', () => { +// const psd: Psd = { +// width: 200, +// height: 200, +// children: [ +// { +// name: 'text layer', +// text: { +// text: 'Hello World\n• c • tiny!\r\ntest', +// // orientation: 'vertical', +// transform: [1, 0, 0, 1, 70, 70], +// style: { +// font: { name: 'ArialMT' }, +// fontSize: 30, +// fillColor: { r: 0, g: 128, b: 0 }, +// }, +// styleRuns: [ +// { length: 12, style: { fillColor: { r: 255, g: 0, b: 0 } } }, +// { length: 12, style: { fillColor: { r: 0, g: 0, b: 255 } } }, +// { length: 4, style: { underline: true } }, +// ], +// paragraphStyle: { +// justification: 'center', +// }, +// warp: { +// style: 'arc', +// value: 50, +// perspective: 0, +// perspectiveOther: 0, +// rotate: 'horizontal', +// }, +// }, +// }, +// { +// name: '2nd layer', +// text: { +// text: 'Aaaaa', +// transform: [1, 0, 0, 1, 70, 70], +// }, +// }, +// ], +// }; + +// fs.writeFileSync(path.join(resultsFilesPath, '_TEXT2.psd'), writePsdBuffer(psd, { logMissingFeatures: true })); +// }); + +// it.skip('read text layer test', () => { +// const psd = readPsdFromFile(path.join(testFilesPath, 'text-test.psd'), opts); +// // const layer = psd.children![1]; + +// // layer.text!.text = 'Foo bar'; +// const buffer = writePsdBuffer(psd, { logMissingFeatures: true }); +// fs.writeFileSync(path.join(resultsFilesPath, '_TEXT.psd'), buffer); + +// // console.log(require('util').inspect(psd.children![0].text, false, 99, true)); +// // console.log(require('util').inspect(psd.children![1].text, false, 99, true)); +// // console.log(require('util').inspect(psd.engineData, false, 99, true)); +// }); + +// it.skip('READ TEST', () => { +// const originalBuffer = fs.readFileSync(path.join(testFilesPath, 'test.psd')); + +// console.log('READING ORIGINAL'); +// const opts = { +// logMissingFeatures: true, +// throwForMissingFeatures: true, +// useImageData: true, +// useRawThumbnail: true, +// logDevFeatures: true, +// }; +// const originalPsd = readPsdInternal(createReaderFromBuffer(originalBuffer), opts); + +// console.log('WRITING'); +// const buffer = writePsdBuffer(originalPsd, { logMissingFeatures: true }); +// fs.writeFileSync('temp.psd', buffer); +// // fs.writeFileSync('temp.bin', buffer); +// // fs.writeFileSync('temp.json', JSON.stringify(originalPsd, null, 2), 'utf8'); +// // fs.writeFileSync('temp.xml', originalPsd.imageResources?.xmpMetadata, 'utf8'); + +// console.log('READING WRITTEN'); +// const psd = readPsdInternal( +// createReaderFromBuffer(buffer), { logMissingFeatures: true, throwForMissingFeatures: true }); + +// clearCanvasFields(originalPsd); +// clearCanvasFields(psd); +// delete originalPsd.imageResources!.thumbnail; +// delete psd.imageResources!.thumbnail; +// delete originalPsd.imageResources!.thumbnailRaw; +// delete psd.imageResources!.thumbnailRaw; +// // console.log(require('util').inspect(originalPsd, false, 99, true)); + +// // fs.writeFileSync('original.json', JSON.stringify(originalPsd, null, 2)); +// // fs.writeFileSync('after.json', JSON.stringify(psd, null, 2)); + +// compareBuffers(buffer, originalBuffer, 'test'); + +// expect(psd).eql(originalPsd); +// }); + +// it.skip('decode engine data 2', () => { +// // const fileData = fs.readFileSync(path.join(__dirname, '..', '..', 'resources', 'engineData2Vertical.txt')); +// const fileData = fs.readFileSync(path.join(__dirname, '..', '..', 'resources', 'engineData2Simple.txt')); +// const func = new Function(`return ${fileData};`); +// const data = func(); +// const result = decodeEngineData2(data); +// fs.writeFileSync( +// path.join(__dirname, '..', '..', 'resources', 'temp.js'), +// 'var x = ' + require('util').inspect(result, false, 99, false), 'utf8'); +// }); + +// it.skip('test.psd', () => { +// const buffer = fs.readFileSync('test.psd'); +// const psd = readPsdInternal(createReaderFromBuffer(buffer), { +// skipCompositeImageData: true, +// skipLayerImageData: true, +// skipThumbnail: true, +// throwForMissingFeatures: true, +// logDevFeatures: true, +// }); +// delete psd.engineData; +// psd.imageResources = {}; +// console.log(require('util').inspect(psd, false, 99, true)); +// }); + +// it.skip('test', () => { +// const psd = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`test/read-write/text-box/src.psd`)), { +// // skipCompositeImageData: true, +// // skipLayerImageData: true, +// // skipThumbnail: true, +// throwForMissingFeatures: true, +// logDevFeatures: true, +// useRawThumbnail: true, +// }); +// fs.writeFileSync('text_rect_out.psd', writePsdBuffer(psd, { logMissingFeatures: true })); +// fs.writeFileSync('text_rect_out.bin', writePsdBuffer(psd, { logMissingFeatures: true })); +// // const psd2 = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`text_rect_out.psd`)), { +// // // skipCompositeImageData: true, +// // // skipLayerImageData: true, +// // // skipThumbnail: true, +// // throwForMissingFeatures: true, +// // logDevFeatures: true, +// // }); +// // psd2; +// const original = fs.readFileSync(`test/read-write/text-box/src.psd`); +// const output = fs.readFileSync(`text_rect_out.psd`); +// compareBuffers(output, original, '-', 0x65d8); // , 0x8ce8, 0x8fca - 0x8ce8); +// }); + +// it.skip('compare test', () => { +// for (const name of ['text_point', 'text_rect']) { +// const psd = readPsdInternal(createReaderFromBuffer(fs.readFileSync(`${name}.psd`)), { +// skipCompositeImageData: true, +// skipLayerImageData: true, +// skipThumbnail: true, +// throwForMissingFeatures: true, +// logDevFeatures: true, +// }); +// // psd.imageResources = {}; +// fs.writeFileSync(`${name}.txt`, require('util').inspect(psd, false, 99, false), 'utf8'); + +// // const engineData = parseEngineData(toByteArray(psd.engineData!)); +// // fs.writeFileSync(`${name}_enginedata.txt`, require('util').inspect(engineData, false, 99, false), 'utf8'); +// } +// }); + +// it.skip('text-replace.psd', () => { +// { +// const buffer = fs.readFileSync('text-replace2.psd'); +// const psd = readPsdInternal(createReaderFromBuffer(buffer), {}); +// psd.children![1]!.text!.text = 'Foo bar'; +// const output = writePsdBuffer(psd, { invalidateTextLayers: true, logMissingFeatures: true }); +// fs.writeFileSync('out.psd', output); +// } + +// { +// const buffer = fs.readFileSync('text-replace.psd'); +// const psd = readPsdInternal(createReaderFromBuffer(buffer), { +// skipCompositeImageData: true, +// skipLayerImageData: true, +// skipThumbnail: true, +// throwForMissingFeatures: true, +// logDevFeatures: true, +// }); +// delete psd.engineData; +// psd.imageResources = {}; +// psd.children?.splice(0, 1); +// fs.writeFileSync('input.txt', require('util').inspect(psd, false, 99, false), 'utf8'); +// } + +// { +// const buffer = fs.readFileSync('out.psd'); +// const psd = readPsdInternal(createReaderFromBuffer(buffer), { +// skipCompositeImageData: true, +// skipLayerImageData: true, +// skipThumbnail: true, +// throwForMissingFeatures: true, +// logDevFeatures: true, +// }); +// delete psd.engineData; +// psd.imageResources = {}; +// psd.children?.splice(0, 1); +// fs.writeFileSync('output.txt', require('util').inspect(psd, false, 99, false), 'utf8'); +// } +// }); +// }); + +// function clearEmptyCanvasFields(layer: Layer | undefined) { +// if (layer) { +// if ('canvas' in layer && !layer.canvas) delete layer.canvas; +// if ('imageData' in layer && !layer.imageData) delete layer.imageData; +// layer.children?.forEach(clearEmptyCanvasFields); +// } +// } + +// function clearCanvasFields(layer: Layer | undefined) { +// if (layer) { +// delete layer.canvas; +// delete layer.imageData; +// if (layer.mask) delete layer.mask.canvas; +// if (layer.mask) delete layer.mask.imageData; +// layer.children?.forEach(clearCanvasFields); +// } +// } diff --git a/src/test/psdWriter.spec.ts b/src/test/psdWriter.spec.ts index c7cf803..9c702a1 100644 --- a/src/test/psdWriter.spec.ts +++ b/src/test/psdWriter.spec.ts @@ -1,499 +1,499 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { expect } from 'chai'; -import { loadCanvasFromFile, compareBuffers, createCanvas, compareCanvases } from './common'; -import { Psd, WriteOptions, ReadOptions, } from '../psd'; -import { writePsd, writeSignature, getWriterBuffer, createWriter } from '../psdWriter'; -import { readPsd, createReader } from '../psdReader'; -import { writePsdBuffer, readPsd as readPsdBuffer } from '../index'; - -const layerImagesPath = path.join(__dirname, '..', '..', 'test', 'layer-images'); -const writeFilesPath = path.join(__dirname, '..', '..', 'test', 'write'); -const resultsFilesPath = path.join(__dirname, '..', '..', 'results'); - -function writeAndRead(psd: Psd, writeOptions: WriteOptions = {}, readOptions: ReadOptions = {}) { - const writer = createWriter(); - writePsd(writer, psd, writeOptions); - const buffer = getWriterBuffer(writer); - const reader = createReader(buffer); - return readPsd(reader, { ...readOptions, throwForMissingFeatures: true, logMissingFeatures: true }); -} - -function tryLoadCanvasFromFile(filePath: string) { - try { - return loadCanvasFromFile(filePath); - } catch { - return undefined; - } -} - -function loadPsdFromJSONAndPNGFiles(basePath: string) { - const psd: Psd = JSON.parse(fs.readFileSync(path.join(basePath, 'data.json'), 'utf8')); - psd.canvas = loadCanvasFromFile(path.join(basePath, 'canvas.png')); - psd.children!.forEach((l, i) => { - if (!l.children) { - l.canvas = tryLoadCanvasFromFile(path.join(basePath, `layer-${i}.png`)); - - if (l.mask) { - l.mask.canvas = tryLoadCanvasFromFile(path.join(basePath, `layer-${i}-mask.png`)); - } - } - }); - psd.linkedFiles?.forEach(f => { - try { - f.data = fs.readFileSync(path.join(basePath, f.name)); - } catch (e) { } - }); - return psd; -} - -describe('PsdWriter', () => { - it('does not throw if writing psd with empty canvas', () => { - const writer = createWriter(); - const psd: Psd = { - width: 300, - height: 200 - }; - - writePsd(writer, psd); - }); - - it('throws if passed invalid signature', () => { - const writer = createWriter(); - - for (const s of ['a', 'ab', 'abcde']) { - expect(() => writeSignature(writer, s), s).throw(`Invalid signature: '${s}'`); - } - }); - - it('throws exception if has layer with both children and canvas properties set', () => { - const writer = createWriter(); - const psd: Psd = { - width: 300, - height: 200, - children: [{ children: [], canvas: createCanvas(300, 300) }] - }; - - expect(() => writePsd(writer, psd)).throw(`Invalid layer, cannot have both 'canvas' and 'children' properties`); - }); - - it('throws exception if has layer with both children and imageData properties set', () => { - const writer = createWriter(); - const psd: Psd = { - width: 300, - height: 200, - children: [{ children: [], imageData: {} as any }] - }; - - expect(() => writePsd(writer, psd)).throw(`Invalid layer, cannot have both 'imageData' and 'children' properties`); - }); - - it('throws if psd has invalid width or height', () => { - const writer = createWriter(); - const psd: Psd = { - width: -5, - height: 0, - }; - - expect(() => writePsd(writer, psd)).throw(`Invalid document size`); - }); - - const fullImage = loadCanvasFromFile(path.join(layerImagesPath, 'full.png')); - const transparentImage = loadCanvasFromFile(path.join(layerImagesPath, 'transparent.png')); - const trimmedImage = loadCanvasFromFile(path.join(layerImagesPath, 'trimmed.png')); - // const croppedImage = loadCanvasFromFile(path.join(layerImagesPath, 'cropped.png')); - // const paddedImage = loadCanvasFromFile(path.join(layerImagesPath, 'padded.png')); - - describe('layer left, top, right, bottom handling', () => { - it('handles undefined left, top, right, bottom with layer image the same size as document', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'full-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(300); - expect(layer.bottom).equal(200); - }); - - it('handles layer image larger than document', () => { - const psd: Psd = { - width: 100, - height: 50, - children: [ - { - name: 'test', - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'oversized-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(300); - expect(layer.bottom).equal(200); - }); - - it('aligns layer image to top left if layer image is smaller than document', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - canvas: trimmedImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(trimmedImage, layer.canvas, 'smaller-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(192); - expect(layer.bottom).equal(68); - }); - - it('does not trim transparent layer image if trim option is not passed', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - canvas: transparentImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(transparentImage, layer.canvas, 'transparent-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(300); - expect(layer.bottom).equal(200); - }); - - it('trims transparent layer image if trim option is set', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - canvas: transparentImage, - }, - ], - }; - - const result = writeAndRead(psd, { trimImageData: true }); - - const layer = result.children![0]; - compareCanvases(trimmedImage, layer.canvas, 'trimmed-layer-image.png'); - expect(layer.left).equal(51); - expect(layer.top).equal(65); - expect(layer.right).equal(243); - expect(layer.bottom).equal(133); - }); - - it('positions the layer at given left/top offsets', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - left: 50, - top: 30, - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'left-top-layer-image.png'); - expect(layer.left).equal(50); - expect(layer.top).equal(30); - expect(layer.right).equal(350); - expect(layer.bottom).equal(230); - }); - - it('ignores right/bottom values', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - right: 200, - bottom: 100, - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'cropped-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(300); - expect(layer.bottom).equal(200); - }); - - it('ignores larger right/bottom values', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - right: 400, - bottom: 250, - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'padded-layer-image.png'); - expect(layer.left).equal(0); - expect(layer.top).equal(0); - expect(layer.right).equal(300); - expect(layer.bottom).equal(200); - }); - - it('ignores right/bottom values if they do not match canvas size', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - left: 50, - top: 50, - right: 50, - bottom: 50, - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'empty-layer-image.png'); - expect(layer.left).equal(50); - expect(layer.top).equal(50); - expect(layer.right).equal(350); - expect(layer.bottom).equal(250); - }); - - it('ignores right/bottom values if they amount to negative size', () => { - const psd: Psd = { - width: 300, - height: 200, - children: [ - { - name: 'test', - left: 50, - top: 50, - right: 0, - bottom: 0, - canvas: fullImage, - }, - ], - }; - - const result = writeAndRead(psd); - - const layer = result.children![0]; - compareCanvases(fullImage, layer.canvas, 'empty-layer-image.png'); - expect(layer.left).equal(50); - expect(layer.top).equal(50); - expect(layer.right).equal(350); - expect(layer.bottom).equal(250); - }); - }); - - it.skip('placedLayer with transform', () => { - const w = 300; - const h = 200; - const psd: Psd = { - width: 1000, - height: 1000, - canvas: createCanvas(1000, 1000), - children: [ - { - name: 'canvas.png', - left: 200, - top: 200, - canvas: createCanvas(600, 600), - placedLayer: { - id: '20953ddb-9391-11ec-b4f1-c15674f50bc4', - placed: 'aaa', - type: 'raster', - transform: [200, 200, 800, 200, 800, 800, 200, 800], - width: w, - height: h, - }, - }, - ], - linkedFiles: [ - { - id: '20953ddb-9391-11ec-b4f1-c15674f50bc4', - name: 'canvas.png', - data: fs.readFileSync(path.join('test', 'write', 'simple', 'canvas.png')), - }, - ], - }; - - const buffer = writePsdBuffer(psd); - fs.writeFileSync(path.join(resultsFilesPath, `placedLayer-with-transform.psd`), buffer); - - // TODO: need to test the file here - - const psd2 = readPsdBuffer(buffer, { throwForMissingFeatures: true, logMissingFeatures: true }); - console.log(require('util').inspect(psd2, false, 99, false), 'utf8'); - }); - - it.skip('vectorMaskFeather', () => { - const psd: Psd = { - width: 821, - height: 523, - children: [ - { - name: 'Circle', - mask: { fromVectorData: true, vectorMaskFeather: 5 }, - vectorFill: { type: 'color', color: { r: 0, g: 0, b: 255 } }, - vectorMask: { - paths: [ - { - fillRule: 'even-odd', - open: true, - operation: 'combine', - knots: [ - { points: [78, 162.1389086942608, 78, 124.02013999999997, 78, 85.90136716453082], linked: false }, - { points: [108.90136716453082, 55, 147.02013999999997, 55, 185.13890869426086, 55], linked: false }, - { points: [216.04027999999994, 85.90136716453082, 216.04027999999994, 124.02013999999997, 216.04027999999994, 162.1389086942608], linked: false }, - { points: [185.13890869426086, 193.04028, 147.02013999999997, 193.04028, 108.90136716453082, 193.04028], linked: false }, - ], - }, - ], - }, - }, - { - name: 'Image', - left: 296, - top: 271, - right: 476, - bottom: 361, - canvas: loadCanvasFromFile('test/gradient.png'), - }, - ], - }; - - const buffer = writePsdBuffer(psd); - fs.writeFileSync(path.join(resultsFilesPath, `vectorMaskFeather.psd`), buffer); - fs.writeFileSync(path.join(resultsFilesPath, `vectorMaskFeather.bin`), buffer); - - const psd2 = readPsdBuffer(buffer, { throwForMissingFeatures: true, logMissingFeatures: true }); - if (0) console.log(require('util').inspect(psd2, false, 99, false), 'utf8'); - }); - - // fs.readdirSync(writeFilesPath).filter(f => /float-size/.test(f)).forEach(f => { - fs.readdirSync(writeFilesPath).filter(f => !/pattern/.test(f)).forEach(f => { - it(`writes PSD file (${f})`, () => { - const compress = f.includes('-compress'); - - const basePath = path.join(writeFilesPath, f); - const psd = loadPsdFromJSONAndPNGFiles(basePath); - - const before = JSON.stringify(psd, replacer); - const buffer = writePsdBuffer(psd, { generateThumbnail: false, trimImageData: true, logMissingFeatures: true, compress }); - const after = JSON.stringify(psd, replacer); - - expect(before).equal(after, 'psd object mutated'); - - const resultsDir = path.join(resultsFilesPath, 'write', f); - fs.mkdirSync(resultsDir, { recursive: true }); - fs.writeFileSync(path.join(resultsDir, `expected.psd`), buffer); - // fs.writeFileSync(path.join(resultsDir, `expected.bin`), buffer); // TEMP - - const reader = createReader(buffer.buffer); - const result = readPsd(reader, { skipLayerImageData: true, logMissingFeatures: true, throwForMissingFeatures: true }); - fs.writeFileSync(path.join(resultsDir, `composite.png`), result.canvas!.toBuffer()); - //compareCanvases(psd.canvas, result.canvas, 'composite image'); - - const expected = fs.readFileSync(path.join(basePath, 'expected.psd')); - compareBuffers(buffer, expected, `ArrayBufferPsdWriter`, 0); - }); - }); - - it.skip('test', () => { - const canvas = createCanvas(5000, 5000); - const context = canvas.getContext('2d')!; - context.fillStyle = 'pink'; - context.fillRect(0, 0, canvas.width, canvas.height); - - const canvas2 = createCanvas(300, 200); - const context2 = canvas2.getContext('2d')!; - context2.fillStyle = 'orange'; - context2.fillRect(0, 0, canvas.width, canvas.height); - - const canvas3 = createCanvas(300, 200); - const context3 = canvas3.getContext('2d')!; - context3.fillStyle = 'red'; - context3.fillRect(0, 0, canvas.width, canvas.height); - - const psd: Psd = { - width: 5000, - height: 5000, - canvas: canvas, - children: [ - { - name: 'bg', - canvas: canvas3, - }, - ], - imageResources: { - thumbnail: canvas2, - }, - }; - - const buffer = writePsdBuffer(psd, { generateThumbnail: false }); - fs.writeFileSync(path.join(resultsFilesPath, `thumb_test.psd`), buffer); - }); -}); - -function replacer(key: string, value: any) { - if (key === 'canvas') { - return ''; - } else { - return value; - } -} +// import * as fs from 'fs'; +// import * as path from 'path'; +// import { expect } from 'chai'; +// import { loadCanvasFromFile, compareBuffers, createCanvas, compareCanvases } from './common'; +// import { Psd, WriteOptions, ReadOptions, } from '../psd'; +// import { writePsd, writeSignature, getWriterBuffer, createWriter } from '../psdWriter'; +// import { readPsd, createReader } from '../psdReader'; +// import { writePsdBuffer, readPsd as readPsdBuffer } from '../index'; + +// const layerImagesPath = path.join(__dirname, '..', '..', 'test', 'layer-images'); +// const writeFilesPath = path.join(__dirname, '..', '..', 'test', 'write'); +// const resultsFilesPath = path.join(__dirname, '..', '..', 'results'); + +// function writeAndRead(psd: Psd, writeOptions: WriteOptions = {}, readOptions: ReadOptions = {}) { +// const writer = createWriter(); +// writePsd(writer, psd, writeOptions); +// const buffer = getWriterBuffer(writer); +// const reader = createReader(buffer); +// return readPsd(reader, { ...readOptions, throwForMissingFeatures: true, logMissingFeatures: true }); +// } + +// function tryLoadCanvasFromFile(filePath: string) { +// try { +// return loadCanvasFromFile(filePath); +// } catch { +// return undefined; +// } +// } + +// function loadPsdFromJSONAndPNGFiles(basePath: string) { +// const psd: Psd = JSON.parse(fs.readFileSync(path.join(basePath, 'data.json'), 'utf8')); +// psd.canvas = loadCanvasFromFile(path.join(basePath, 'canvas.png')); +// psd.children!.forEach((l, i) => { +// if (!l.children) { +// l.canvas = tryLoadCanvasFromFile(path.join(basePath, `layer-${i}.png`)); + +// if (l.mask) { +// l.mask.canvas = tryLoadCanvasFromFile(path.join(basePath, `layer-${i}-mask.png`)); +// } +// } +// }); +// psd.linkedFiles?.forEach(f => { +// try { +// f.data = fs.readFileSync(path.join(basePath, f.name)); +// } catch (e) { } +// }); +// return psd; +// } + +// describe('PsdWriter', () => { +// it('does not throw if writing psd with empty canvas', () => { +// const writer = createWriter(); +// const psd: Psd = { +// width: 300, +// height: 200 +// }; + +// writePsd(writer, psd); +// }); + +// it('throws if passed invalid signature', () => { +// const writer = createWriter(); + +// for (const s of ['a', 'ab', 'abcde']) { +// expect(() => writeSignature(writer, s), s).throw(`Invalid signature: '${s}'`); +// } +// }); + +// it('throws exception if has layer with both children and canvas properties set', () => { +// const writer = createWriter(); +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [{ children: [], canvas: createCanvas(300, 300) }] +// }; + +// expect(() => writePsd(writer, psd)).throw(`Invalid layer, cannot have both 'canvas' and 'children' properties`); +// }); + +// it('throws exception if has layer with both children and imageData properties set', () => { +// const writer = createWriter(); +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [{ children: [], imageData: {} as any }] +// }; + +// expect(() => writePsd(writer, psd)).throw(`Invalid layer, cannot have both 'imageData' and 'children' properties`); +// }); + +// it('throws if psd has invalid width or height', () => { +// const writer = createWriter(); +// const psd: Psd = { +// width: -5, +// height: 0, +// }; + +// expect(() => writePsd(writer, psd)).throw(`Invalid document size`); +// }); + +// const fullImage = loadCanvasFromFile(path.join(layerImagesPath, 'full.png')); +// const transparentImage = loadCanvasFromFile(path.join(layerImagesPath, 'transparent.png')); +// const trimmedImage = loadCanvasFromFile(path.join(layerImagesPath, 'trimmed.png')); +// // const croppedImage = loadCanvasFromFile(path.join(layerImagesPath, 'cropped.png')); +// // const paddedImage = loadCanvasFromFile(path.join(layerImagesPath, 'padded.png')); + +// describe('layer left, top, right, bottom handling', () => { +// it('handles undefined left, top, right, bottom with layer image the same size as document', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'full-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(300); +// expect(layer.bottom).equal(200); +// }); + +// it('handles layer image larger than document', () => { +// const psd: Psd = { +// width: 100, +// height: 50, +// children: [ +// { +// name: 'test', +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'oversized-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(300); +// expect(layer.bottom).equal(200); +// }); + +// it('aligns layer image to top left if layer image is smaller than document', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// canvas: trimmedImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(trimmedImage, layer.canvas, 'smaller-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(192); +// expect(layer.bottom).equal(68); +// }); + +// it('does not trim transparent layer image if trim option is not passed', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// canvas: transparentImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(transparentImage, layer.canvas, 'transparent-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(300); +// expect(layer.bottom).equal(200); +// }); + +// it('trims transparent layer image if trim option is set', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// canvas: transparentImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd, { trimImageData: true }); + +// const layer = result.children![0]; +// compareCanvases(trimmedImage, layer.canvas, 'trimmed-layer-image.png'); +// expect(layer.left).equal(51); +// expect(layer.top).equal(65); +// expect(layer.right).equal(243); +// expect(layer.bottom).equal(133); +// }); + +// it('positions the layer at given left/top offsets', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// left: 50, +// top: 30, +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'left-top-layer-image.png'); +// expect(layer.left).equal(50); +// expect(layer.top).equal(30); +// expect(layer.right).equal(350); +// expect(layer.bottom).equal(230); +// }); + +// it('ignores right/bottom values', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// right: 200, +// bottom: 100, +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'cropped-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(300); +// expect(layer.bottom).equal(200); +// }); + +// it('ignores larger right/bottom values', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// right: 400, +// bottom: 250, +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'padded-layer-image.png'); +// expect(layer.left).equal(0); +// expect(layer.top).equal(0); +// expect(layer.right).equal(300); +// expect(layer.bottom).equal(200); +// }); + +// it('ignores right/bottom values if they do not match canvas size', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// left: 50, +// top: 50, +// right: 50, +// bottom: 50, +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'empty-layer-image.png'); +// expect(layer.left).equal(50); +// expect(layer.top).equal(50); +// expect(layer.right).equal(350); +// expect(layer.bottom).equal(250); +// }); + +// it('ignores right/bottom values if they amount to negative size', () => { +// const psd: Psd = { +// width: 300, +// height: 200, +// children: [ +// { +// name: 'test', +// left: 50, +// top: 50, +// right: 0, +// bottom: 0, +// canvas: fullImage, +// }, +// ], +// }; + +// const result = writeAndRead(psd); + +// const layer = result.children![0]; +// compareCanvases(fullImage, layer.canvas, 'empty-layer-image.png'); +// expect(layer.left).equal(50); +// expect(layer.top).equal(50); +// expect(layer.right).equal(350); +// expect(layer.bottom).equal(250); +// }); +// }); + +// it.skip('placedLayer with transform', () => { +// const w = 300; +// const h = 200; +// const psd: Psd = { +// width: 1000, +// height: 1000, +// canvas: createCanvas(1000, 1000), +// children: [ +// { +// name: 'canvas.png', +// left: 200, +// top: 200, +// canvas: createCanvas(600, 600), +// placedLayer: { +// id: '20953ddb-9391-11ec-b4f1-c15674f50bc4', +// placed: 'aaa', +// type: 'raster', +// transform: [200, 200, 800, 200, 800, 800, 200, 800], +// width: w, +// height: h, +// }, +// }, +// ], +// linkedFiles: [ +// { +// id: '20953ddb-9391-11ec-b4f1-c15674f50bc4', +// name: 'canvas.png', +// data: fs.readFileSync(path.join('test', 'write', 'simple', 'canvas.png')), +// }, +// ], +// }; + +// const buffer = writePsdBuffer(psd); +// fs.writeFileSync(path.join(resultsFilesPath, `placedLayer-with-transform.psd`), buffer); + +// // TODO: need to test the file here + +// const psd2 = readPsdBuffer(buffer, { throwForMissingFeatures: true, logMissingFeatures: true }); +// console.log(require('util').inspect(psd2, false, 99, false), 'utf8'); +// }); + +// it.skip('vectorMaskFeather', () => { +// const psd: Psd = { +// width: 821, +// height: 523, +// children: [ +// { +// name: 'Circle', +// mask: { fromVectorData: true, vectorMaskFeather: 5 }, +// vectorFill: { type: 'color', color: { r: 0, g: 0, b: 255 } }, +// vectorMask: { +// paths: [ +// { +// fillRule: 'even-odd', +// open: true, +// operation: 'combine', +// knots: [ +// { points: [78, 162.1389086942608, 78, 124.02013999999997, 78, 85.90136716453082], linked: false }, +// { points: [108.90136716453082, 55, 147.02013999999997, 55, 185.13890869426086, 55], linked: false }, +// { points: [216.04027999999994, 85.90136716453082, 216.04027999999994, 124.02013999999997, 216.04027999999994, 162.1389086942608], linked: false }, +// { points: [185.13890869426086, 193.04028, 147.02013999999997, 193.04028, 108.90136716453082, 193.04028], linked: false }, +// ], +// }, +// ], +// }, +// }, +// { +// name: 'Image', +// left: 296, +// top: 271, +// right: 476, +// bottom: 361, +// canvas: loadCanvasFromFile('test/gradient.png'), +// }, +// ], +// }; + +// const buffer = writePsdBuffer(psd); +// fs.writeFileSync(path.join(resultsFilesPath, `vectorMaskFeather.psd`), buffer); +// fs.writeFileSync(path.join(resultsFilesPath, `vectorMaskFeather.bin`), buffer); + +// const psd2 = readPsdBuffer(buffer, { throwForMissingFeatures: true, logMissingFeatures: true }); +// if (0) console.log(require('util').inspect(psd2, false, 99, false), 'utf8'); +// }); + +// // fs.readdirSync(writeFilesPath).filter(f => /float-size/.test(f)).forEach(f => { +// fs.readdirSync(writeFilesPath).filter(f => !/pattern/.test(f)).forEach(f => { +// it(`writes PSD file (${f})`, () => { +// const compress = f.includes('-compress'); + +// const basePath = path.join(writeFilesPath, f); +// const psd = loadPsdFromJSONAndPNGFiles(basePath); + +// const before = JSON.stringify(psd, replacer); +// const buffer = writePsdBuffer(psd, { generateThumbnail: false, trimImageData: true, logMissingFeatures: true, compress }); +// const after = JSON.stringify(psd, replacer); + +// expect(before).equal(after, 'psd object mutated'); + +// const resultsDir = path.join(resultsFilesPath, 'write', f); +// fs.mkdirSync(resultsDir, { recursive: true }); +// fs.writeFileSync(path.join(resultsDir, `expected.psd`), buffer); +// // fs.writeFileSync(path.join(resultsDir, `expected.bin`), buffer); // TEMP + +// const reader = createReader(buffer.buffer); +// const result = readPsd(reader, { skipLayerImageData: true, logMissingFeatures: true, throwForMissingFeatures: true }); +// fs.writeFileSync(path.join(resultsDir, `composite.png`), result.canvas!.toBuffer()); +// //compareCanvases(psd.canvas, result.canvas, 'composite image'); + +// const expected = fs.readFileSync(path.join(basePath, 'expected.psd')); +// compareBuffers(buffer, expected, `ArrayBufferPsdWriter`, 0); +// }); +// }); + +// it.skip('test', () => { +// const canvas = createCanvas(5000, 5000); +// const context = canvas.getContext('2d')!; +// context.fillStyle = 'pink'; +// context.fillRect(0, 0, canvas.width, canvas.height); + +// const canvas2 = createCanvas(300, 200); +// const context2 = canvas2.getContext('2d')!; +// context2.fillStyle = 'orange'; +// context2.fillRect(0, 0, canvas.width, canvas.height); + +// const canvas3 = createCanvas(300, 200); +// const context3 = canvas3.getContext('2d')!; +// context3.fillStyle = 'red'; +// context3.fillRect(0, 0, canvas.width, canvas.height); + +// const psd: Psd = { +// width: 5000, +// height: 5000, +// canvas: canvas, +// children: [ +// { +// name: 'bg', +// canvas: canvas3, +// }, +// ], +// imageResources: { +// thumbnail: canvas2, +// }, +// }; + +// const buffer = writePsdBuffer(psd, { generateThumbnail: false }); +// fs.writeFileSync(path.join(resultsFilesPath, `thumb_test.psd`), buffer); +// }); +// }); + +// function replacer(key: string, value: any) { +// if (key === 'canvas') { +// return ''; +// } else { +// return value; +// } +// }