import { Psd, Layer, LayerAdditionalInfo, ColorMode, SectionDividerType, WriteOptions, Color, GlobalLayerMaskInfo, PixelData, LayerMaskData, Compression, ChannelID } from './psd'; import { hasAlpha, createCanvas, writeDataRLE, LayerChannelData, ChannelData, offsetForChannel, createImageData, fromBlendMode, clamp, LayerMaskFlags, MaskParams, ColorSpace, Bounds, largeAdditionalInfoKeys, RAW_IMAGE_DATA, writeDataZipWithoutPrediction, imageDataToCanvas } from './helpers'; import { ExtendedWriteOptions, infoHandlers } from './additionalInfo'; import { InternalImageResources, resourceHandlers } from './imageResources'; export interface PsdWriter { offset: number; buffer: ArrayBuffer; view: DataView; tempBuffer: Uint8Array | undefined; } export function createWriter(size = 4096): PsdWriter { const buffer = new ArrayBuffer(size); const view = new DataView(buffer); const offset = 0; return { buffer, view, offset, tempBuffer: undefined }; } export function getWriterBuffer(writer: PsdWriter) { return writer.buffer.slice(0, writer.offset); } export function getWriterBufferNoCopy(writer: PsdWriter) { return new Uint8Array(writer.buffer, 0, writer.offset); } export function writeUint8(writer: PsdWriter, value: number) { const offset = addSize(writer, 1); writer.view.setUint8(offset, value); } export function writeInt16(writer: PsdWriter, value: number) { const offset = addSize(writer, 2); writer.view.setInt16(offset, value, false); } export function writeUint16(writer: PsdWriter, value: number) { const offset = addSize(writer, 2); writer.view.setUint16(offset, value, false); } export function writeUint16LE(writer: PsdWriter, value: number) { const offset = addSize(writer, 2); writer.view.setUint16(offset, value, true); } export function writeInt32(writer: PsdWriter, value: number) { const offset = addSize(writer, 4); writer.view.setInt32(offset, value, false); } export function writeInt32LE(writer: PsdWriter, value: number) { const offset = addSize(writer, 4); writer.view.setInt32(offset, value, true); } export function writeUint32(writer: PsdWriter, value: number) { const offset = addSize(writer, 4); writer.view.setUint32(offset, value, false); } export function writeFloat32(writer: PsdWriter, value: number) { const offset = addSize(writer, 4); writer.view.setFloat32(offset, value, false); } export function writeFloat64(writer: PsdWriter, value: number) { const offset = addSize(writer, 8); writer.view.setFloat64(offset, value, false); } // 32-bit fixed-point number 16.16 export function writeFixedPoint32(writer: PsdWriter, value: number) { writeInt32(writer, value * (1 << 16)); } // 32-bit fixed-point number 8.24 export function writeFixedPointPath32(writer: PsdWriter, value: number) { writeInt32(writer, value * (1 << 24)); } export function writeBytes(writer: PsdWriter, buffer: Uint8Array | undefined) { if (buffer) { ensureSize(writer, writer.offset + buffer.length); const bytes = new Uint8Array(writer.buffer); bytes.set(buffer, writer.offset); writer.offset += buffer.length; } } export function writeZeros(writer: PsdWriter, count: number) { for (let i = 0; i < count; i++) { writeUint8(writer, 0); } } export function writeSignature(writer: PsdWriter, signature: string) { if (signature.length !== 4) throw new Error(`Invalid signature: '${signature}'`); for (let i = 0; i < 4; i++) { writeUint8(writer, signature.charCodeAt(i)); } } export function writeAsciiString(writer: PsdWriter, text: string) { for (let i = 0; i < text.length; i++) { writeUint8(writer, text.charCodeAt(i)); } } export function writePascalString(writer: PsdWriter, text: string, padTo: number) { let length = text.length; if (length > 255) throw new Error(`String too long`); writeUint8(writer, length); for (let i = 0; i < length; i++) { const code = text.charCodeAt(i); // writeUint8(writer, code); // for testing writeUint8(writer, code < 128 ? code : '?'.charCodeAt(0)); } while (++length % padTo) { writeUint8(writer, 0); } } export function writeUnicodeStringWithoutLength(writer: PsdWriter, text: string) { for (let i = 0; i < text.length; i++) { writeUint16(writer, text.charCodeAt(i)); } } export function writeUnicodeStringWithoutLengthLE(writer: PsdWriter, text: string) { for (let i = 0; i < text.length; i++) { writeUint16LE(writer, text.charCodeAt(i)); } } export function writeUnicodeString(writer: PsdWriter, text: string) { writeUint32(writer, text.length); writeUnicodeStringWithoutLength(writer, text); } export function writeUnicodeStringWithPadding(writer: PsdWriter, text: string) { writeUint32(writer, text.length + 1); for (let i = 0; i < text.length; i++) { writeUint16(writer, text.charCodeAt(i)); } writeUint16(writer, 0); } function getLargestLayerSize(layers: Layer[] = []): number { let max = 0; for (const layer of layers) { if (layer.canvas || layer.imageData) { const { width, height } = getLayerDimentions(layer); max = Math.max(max, 2 * height + 2 * width * height); } if (layer.children) { max = Math.max(max, getLargestLayerSize(layer.children)); } } return max; } export function writeSection(writer: PsdWriter, round: number, func: () => void, writeTotalLength = false, large = false) { if (large) writeUint32(writer, 0); const offset = writer.offset; writeUint32(writer, 0); func(); let length = writer.offset - offset - 4; let len = length; while (len % round) { writeUint8(writer, 0); len++; } // while (writer.offset % round) { // writeUint8(writer, 0); // len++; // } if (writeTotalLength) { length = len; } writer.view.setUint32(offset, length, false); } function verifyBitCount(target: Psd | Layer) { target.children?.forEach(verifyBitCount); const data = target.imageData; if (data && (data.data instanceof Uint32Array || data.data instanceof Uint16Array)) { throw new Error('imageData has incorrect bitDepth'); } if ('mask' in target && target.mask) { const data = target.mask.imageData; if (data && (data.data instanceof Uint32Array || data.data instanceof Uint16Array)) { throw new Error('mask imageData has incorrect bitDepth'); } } } export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}) { if (!(+psd.width > 0 && +psd.height > 0)) throw new Error('Invalid document size'); if ((psd.width > 30000 || psd.height > 30000) && !options.psb) throw new Error('Document size is too large (max is 30000x30000, use PSB format instead)'); const bitsPerChannel = psd.bitsPerChannel ?? 8; if (bitsPerChannel !== 8) throw new Error('bitsPerChannel other than 8 are not supported for writing'); verifyBitCount(psd); const imageResources: InternalImageResources = { ...psd.imageResources }; const opt: ExtendedWriteOptions = { ...options, layerIds: new Set(), layerToId: new Map() }; if (opt.generateThumbnail) { imageResources.thumbnail = createThumbnail(psd); } let imageData = psd.imageData; if (!imageData && psd.canvas) { imageData = psd.canvas.getContext('2d')!.getImageData(0, 0, psd.canvas.width, psd.canvas.height); } if (imageData && (psd.width !== imageData.width || psd.height !== imageData.height)) throw new Error('Document canvas must have the same size as document'); const globalAlpha = !!imageData && hasAlpha(imageData); const maxBufferSize = Math.max(getLargestLayerSize(psd.children), 4 * 2 * psd.width * psd.height + 2 * psd.height); writer.tempBuffer = new Uint8Array(maxBufferSize); // header writeSignature(writer, '8BPS'); writeUint16(writer, options.psb ? 2 : 1); // version writeZeros(writer, 6); writeUint16(writer, globalAlpha ? 4 : 3); // channels writeUint32(writer, psd.height); writeUint32(writer, psd.width); writeUint16(writer, bitsPerChannel); // bits per channel writeUint16(writer, ColorMode.RGB); // we only support saving RGB right now // color mode data writeSection(writer, 1, () => { if (psd.palette) { for (let i = 0; i < 256; i++) writeUint8(writer, psd.palette[i]?.r || 0); for (let i = 0; i < 256; i++) writeUint8(writer, psd.palette[i]?.g || 0); for (let i = 0; i < 256; i++) writeUint8(writer, psd.palette[i]?.b || 0); } // TODO: other data? }); const layers: Layer[] = []; addChildren(layers, psd.children); if (!layers.length) layers.push({}); // image resources imageResources.layersGroup = layers.map(l => l.linkGroup || 0); imageResources.layerGroupsEnabledId = layers.map(l => l.linkGroupEnabled == false ? 0 : 1); writeSection(writer, 1, () => { for (const handler of resourceHandlers) { const has = handler.has(imageResources); const count = has === false ? 0 : (has === true ? 1 : has); for (let i = 0; i < count; i++) { writeSignature(writer, '8BIM'); writeUint16(writer, handler.key); writePascalString(writer, '', 2); writeSection(writer, 2, () => handler.write(writer, imageResources, i)); } } }); // layer and mask info writeSection(writer, 2, () => { writeLayerInfo(writer, layers, psd, globalAlpha, opt); writeGlobalLayerMaskInfo(writer, psd.globalLayerMaskInfo); writeAdditionalLayerInfo(writer, psd, psd, opt); }, undefined, !!opt.psb); // image data const channels = globalAlpha ? [0, 1, 2, 3] : [0, 1, 2]; const width = imageData ? imageData.width : psd.width; const height = imageData ? imageData.height : psd.height; const data: PixelData = { data: new Uint8Array(width * height * 4), width, height }; writeUint16(writer, Compression.RleCompressed); // Photoshop doesn't support zip compression of composite image data if (RAW_IMAGE_DATA && (psd as any).imageDataRaw) { console.log('writing raw image data'); writeBytes(writer, (psd as any).imageDataRaw); } else { if (imageData) data.data.set(new Uint8Array(imageData.data.buffer, imageData.data.byteOffset, imageData.data.byteLength)); // add weird white matte if (globalAlpha) { const size = data.width * data.height * 4; const p = data.data; for (let i = 0; i < size; i += 4) { const pa = p[i + 3]; if (pa != 0 && pa != 255) { const a = pa / 255; const ra = 255 * (1 - a); p[i + 0] = p[i + 0] * a + ra; p[i + 1] = p[i + 1] * a + ra; p[i + 2] = p[i + 2] * a + ra; } } } writeBytes(writer, writeDataRLE(writer.tempBuffer, data, channels, !!options.psb)); } } function writeLayerInfo(writer: PsdWriter, layers: Layer[], psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) { writeSection(writer, 4, () => { writeInt16(writer, globalAlpha ? -layers.length : layers.length); const layersData = layers.map((l, i) => getChannels(writer.tempBuffer!, l, i === 0, options)); // layer records for (const layerData of layersData) { const { layer, top, left, bottom, right, channels } = layerData; writeInt32(writer, top); writeInt32(writer, left); writeInt32(writer, bottom); writeInt32(writer, right); writeUint16(writer, channels.length); for (const c of channels) { writeInt16(writer, c.channelId); if (options.psb) writeUint32(writer, 0); writeUint32(writer, c.length); } writeSignature(writer, '8BIM'); writeSignature(writer, fromBlendMode[layer.blendMode!] || 'norm'); writeUint8(writer, Math.round(clamp(layer.opacity ?? 1, 0, 1) * 255)); writeUint8(writer, layer.clipping ? 1 : 0); let flags = 0x08; // 1 for Photoshop 5.0 and later, tells if bit 4 has useful information if (layer.transparencyProtected) flags |= 0x01; if (layer.hidden) flags |= 0x02; if (layer.vectorMask || (layer.sectionDivider && layer.sectionDivider.type !== SectionDividerType.Other) || layer.adjustment) { flags |= 0x10; // pixel data irrelevant to appearance of document } if (layer.effectsOpen) flags |= 0x20; writeUint8(writer, flags); writeUint8(writer, 0); // filler writeSection(writer, 1, () => { writeLayerMaskData(writer, layer, layerData); writeLayerBlendingRanges(writer, layer); writePascalString(writer, (layer.name || '').substring(0, 255), 4); writeAdditionalLayerInfo(writer, layer, psd, options); }); } // layer channel image data for (const layerData of layersData) { for (const channel of layerData.channels) { writeUint16(writer, channel.compression); if (channel.buffer) { writeBytes(writer, channel.buffer); } } } }, true, options.psb); } function writeLayerMaskData(writer: PsdWriter, { mask, realMask }: Layer, layerData: LayerChannelData) { writeSection(writer, 1, () => { if (!mask && !realMask) return; let params = 0, flags = 0, realFlags = 0; if (mask) { if (mask.userMaskDensity !== undefined) params |= MaskParams.UserMaskDensity; if (mask.userMaskFeather !== undefined) params |= MaskParams.UserMaskFeather; if (mask.vectorMaskDensity !== undefined) params |= MaskParams.VectorMaskDensity; if (mask.vectorMaskFeather !== undefined) params |= MaskParams.VectorMaskFeather; if (mask.disabled) flags |= LayerMaskFlags.LayerMaskDisabled; if (mask.positionRelativeToLayer) flags |= LayerMaskFlags.PositionRelativeToLayer; if (mask.fromVectorData) flags |= LayerMaskFlags.LayerMaskFromRenderingOtherData; if (params) flags |= LayerMaskFlags.MaskHasParametersAppliedToIt; } const m = layerData.mask || {} as Partial; writeInt32(writer, m.top || 0); writeInt32(writer, m.left || 0); writeInt32(writer, m.bottom || 0); writeInt32(writer, m.right || 0); writeUint8(writer, mask && mask.defaultColor || 0); writeUint8(writer, flags); if (realMask) { if (realMask.disabled) realFlags |= LayerMaskFlags.LayerMaskDisabled; if (realMask.positionRelativeToLayer) realFlags |= LayerMaskFlags.PositionRelativeToLayer; if (realMask.fromVectorData) realFlags |= LayerMaskFlags.LayerMaskFromRenderingOtherData; const r = layerData.realMask || {} as Partial; writeUint8(writer, realFlags); writeUint8(writer, realMask.defaultColor || 0); writeInt32(writer, r.top || 0); writeInt32(writer, r.left || 0); writeInt32(writer, r.bottom || 0); writeInt32(writer, r.right || 0); } if (params && mask) { writeUint8(writer, params); if (mask.userMaskDensity !== undefined) writeUint8(writer, Math.round(mask.userMaskDensity * 0xff)); if (mask.userMaskFeather !== undefined) writeFloat64(writer, mask.userMaskFeather); if (mask.vectorMaskDensity !== undefined) writeUint8(writer, Math.round(mask.vectorMaskDensity * 0xff)); if (mask.vectorMaskFeather !== undefined) writeFloat64(writer, mask.vectorMaskFeather); } writeZeros(writer, 2); }); } function writerBlendingRange(writer: PsdWriter, range: number[]) { writeUint8(writer, range[0]); writeUint8(writer, range[1]); writeUint8(writer, range[2]); writeUint8(writer, range[3]); } function writeLayerBlendingRanges(writer: PsdWriter, layer: Layer) { writeSection(writer, 1, () => { const ranges = layer.blendingRanges; if (ranges) { writerBlendingRange(writer, ranges.compositeGrayBlendSource); writerBlendingRange(writer, ranges.compositeGraphBlendDestinationRange); for (const r of ranges.ranges) { writerBlendingRange(writer, r.sourceRange); writerBlendingRange(writer, r.destRange); } } }); } function writeGlobalLayerMaskInfo(writer: PsdWriter, info: GlobalLayerMaskInfo | undefined) { writeSection(writer, 1, () => { if (info) { writeUint16(writer, info.overlayColorSpace); writeUint16(writer, info.colorSpace1); writeUint16(writer, info.colorSpace2); writeUint16(writer, info.colorSpace3); writeUint16(writer, info.colorSpace4); writeUint16(writer, info.opacity * 0xff); writeUint8(writer, info.kind); writeZeros(writer, 3); } }); } function writeAdditionalLayerInfo(writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) { for (const handler of infoHandlers) { let key = handler.key; if (key === 'Txt2' && options.invalidateTextLayers) continue; if (key === 'vmsk' && options.psb) key = 'vsms'; if (handler.has(target)) { const large = options.psb && largeAdditionalInfoKeys.indexOf(key) !== -1; const writeTotalLength = key !== 'Txt2' && key !== 'cinf' && key !== 'extn' && key !== 'CAI ' && key !== 'OCIO'; const fourBytes = key === 'Txt2' || key === 'luni' || key === 'vmsk' || key === 'artb' || key === 'artd' || key === 'vogk' || key === 'SoLd' || key === 'lnk2' || key === 'vscg' || key === 'vsms' || key === 'GdFl' || key === 'lmfx' || key === 'lrFX' || key === 'cinf' || key === 'PlLd' || key === 'Anno' || key === 'CAI ' || key === 'OCIO' || key === 'GenI' || key === 'FEid' || key === 'curv' || key === 'CgEd' || key === 'vibA' || key === 'blwh' || key === 'grdm'; writeSignature(writer, large ? '8B64' : '8BIM'); writeSignature(writer, key); writeSection(writer, fourBytes ? 4 : 2, () => { handler.write(writer, target, psd, options); }, writeTotalLength, large); } } } function addChildren(layers: Layer[], children: Layer[] | undefined) { if (!children) return; // const layerIds: number[] = [2]; // const timestamps: number[] = [1740120767.0230637]; for (const c of children) { if (c.children && c.canvas) throw new Error(`Invalid layer, cannot have both 'canvas' and 'children' properties`); if (c.children && c.imageData) throw new Error(`Invalid layer, cannot have both 'imageData' and 'children' properties`); if (c.children) { layers.push({ name: '', sectionDivider: { type: SectionDividerType.BoundingSectionDivider, }, // blendingRanges: children[0].blendingRanges, // nameSource: 'lset', // id: layerIds.shift(), // protected: { // transparency: false, // composite: false, // position: false, // }, // layerColor: 'red', // timestamp: timestamps.shift(), // referencePoint: { x: 0, y: 0 }, }); addChildren(layers, c.children); layers.push({ ...c, blendMode: c.blendMode === 'pass through' ? 'normal' : c.blendMode, sectionDivider: { type: c.opened === false ? SectionDividerType.ClosedFolder : SectionDividerType.OpenFolder, key: fromBlendMode[c.blendMode!] || 'pass', subType: 0, }, }); } else { layers.push({ ...c }); } } } function resizeBuffer(writer: PsdWriter, size: number) { let newLength = writer.buffer.byteLength; do { newLength *= 2; } while (size > newLength); const newBuffer = new ArrayBuffer(newLength); const newBytes = new Uint8Array(newBuffer); const oldBytes = new Uint8Array(writer.buffer); newBytes.set(oldBytes); writer.buffer = newBuffer; writer.view = new DataView(writer.buffer); } function ensureSize(writer: PsdWriter, size: number) { if (size > writer.buffer.byteLength) { resizeBuffer(writer, size); } } function addSize(writer: PsdWriter, size: number) { const offset = writer.offset; ensureSize(writer, writer.offset += size); return offset; } function createThumbnail(psd: Psd) { const canvas = createCanvas(10, 10); let scale = 1; if (psd.width > psd.height) { canvas.width = 160; canvas.height = Math.floor(psd.height * (canvas.width / psd.width)); scale = canvas.width / psd.width; } else { canvas.height = 160; canvas.width = Math.floor(psd.width * (canvas.height / psd.height)); scale = canvas.height / psd.height; } const context = canvas.getContext('2d')!; context.scale(scale, scale); if (psd.imageData) { context.drawImage(imageDataToCanvas(psd.imageData), 0, 0); } else if (psd.canvas) { context.drawImage(psd.canvas, 0, 0); } return canvas; } function getMaskChannels(tempBuffer: Uint8Array, layerData: LayerChannelData, layer: Layer, mask: LayerMaskData, options: WriteOptions, realMask: boolean) { let top = (mask.top as any) | 0; let left = (mask.left as any) | 0; let right = (mask.right as any) | 0; let bottom = (mask.bottom as any) | 0; let { width, height } = getLayerDimentions(mask); let imageData = mask.imageData; if (!imageData && mask.canvas && width && height) { imageData = mask.canvas.getContext('2d')!.getImageData(0, 0, width, height); } if (imageData && (imageData.width !== width || imageData.height !== height)) { throw new Error('Invalid imageData dimentions'); } right = left + width; bottom = top + height; let buffer: Uint8Array; let compression: Compression; if (RAW_IMAGE_DATA && (layer as any)[realMask ? 'realMaskDataRaw' : 'maskDataRaw']) { buffer = (layer as any)[realMask ? 'realMaskDataRaw' : 'maskDataRaw']; compression = (layer as any)[realMask ? 'realMaskDataRawCompression' : 'maskDataRawCompression']; } else if (!imageData) { buffer = new Uint8Array(0); compression = Compression.RleCompressed; } else if (options.compress) { buffer = writeDataZipWithoutPrediction(imageData, [0]); compression = Compression.ZipWithoutPrediction; } else { buffer = writeDataRLE(tempBuffer, imageData, [0], !!options.psb)!; compression = Compression.RleCompressed; } layerData.channels.push({ channelId: realMask ? ChannelID.RealUserMask : ChannelID.UserMask, compression, buffer, length: 2 + buffer.length }); layerData[realMask ? 'realMask' : 'mask'] = { top, left, right, bottom }; } function getChannels(tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions): LayerChannelData { const layerData = getLayerChannels(tempBuffer, layer, background, options); if (layer.mask) getMaskChannels(tempBuffer, layerData, layer, layer.mask, options, false); if (layer.realMask) getMaskChannels(tempBuffer, layerData, layer, layer.realMask, options, true); return layerData; } function getLayerDimentions({ canvas, imageData }: Layer): { width: number; height: number; } { // this way in case canvas/imageData are incorrect objects without width/height properties const width = imageData?.width ?? canvas?.width ?? 0; const height = imageData?.height ?? canvas?.height ?? 0; return { width, height }; } function cropImageData(data: PixelData, left: number, top: number, width: number, height: number) { if (data.data instanceof Uint32Array || data.data instanceof Uint16Array) { throw new Error('imageData has incorrect bit depth'); } const croppedData = createImageData(width, height); const srcData = data.data; const dstData = croppedData.data; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let src = ((x + left) + (y + top) * data.width) * 4; let dst = (x + y * width) * 4; dstData[dst] = srcData[src]; dstData[dst + 1] = srcData[src + 1]; dstData[dst + 2] = srcData[src + 2]; dstData[dst + 3] = srcData[src + 3]; } } return croppedData; } function getLayerChannels(tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions): LayerChannelData { let top = (layer.top as any) | 0; let left = (layer.left as any) | 0; let right = (layer.right as any) | 0; let bottom = (layer.bottom as any) | 0; let channels: ChannelData[] = [ { channelId: ChannelID.Transparency, compression: Compression.RawData, buffer: undefined, length: 2 }, { channelId: ChannelID.Color0, compression: Compression.RawData, buffer: undefined, length: 2 }, { channelId: ChannelID.Color1, compression: Compression.RawData, buffer: undefined, length: 2 }, { channelId: ChannelID.Color2, compression: Compression.RawData, buffer: undefined, length: 2 }, ]; let { width, height } = getLayerDimentions(layer); if (!(layer.canvas || layer.imageData) || !width || !height) { right = left; bottom = top; return { layer, top, left, right, bottom, channels }; } right = left + width; bottom = top + height; let data = layer.imageData || layer.canvas!.getContext('2d')!.getImageData(0, 0, width, height); if (options.trimImageData) { const trimmed = trimData(data); if (trimmed.left !== 0 || trimmed.top !== 0 || trimmed.right !== data.width || trimmed.bottom !== data.height) { left += trimmed.left; top += trimmed.top; right -= (data.width - trimmed.right); bottom -= (data.height - trimmed.bottom); width = right - left; height = bottom - top; if (!width || !height) return { layer, top, left, right, bottom, channels }; data = cropImageData(data, trimmed.left, trimmed.top, width, height); } } const channelIds = [ ChannelID.Color0, ChannelID.Color1, ChannelID.Color2, ]; if (!background || options.noBackground || layer.mask || hasAlpha(data) || (RAW_IMAGE_DATA && (layer as any).imageDataRaw?.['-1'])) { channelIds.unshift(ChannelID.Transparency); } channels = channelIds.map(channelId => { const offset = offsetForChannel(channelId, false); // TODO: psd.colorMode === ColorMode.CMYK); let buffer: Uint8Array; let compression: Compression; if (RAW_IMAGE_DATA && (layer as any).imageDataRaw) { // console.log('written raw layer image data'); buffer = (layer as any).imageDataRaw[channelId]; compression = (layer as any).imageDataRawCompression[channelId]; } else if (options.compress) { buffer = writeDataZipWithoutPrediction(data, [offset]); compression = Compression.ZipWithoutPrediction; } else { buffer = writeDataRLE(tempBuffer, data, [offset], !!options.psb)!; compression = Compression.RleCompressed; } return { channelId, compression, buffer, length: 2 + buffer.length }; }); return { layer, top, left, right, bottom, channels }; } function isRowEmpty({ data, width }: PixelData, y: number, left: number, right: number) { const start = ((y * width + left) * 4 + 3) | 0; const end = (start + (right - left) * 4) | 0; for (let i = start; i < end; i = (i + 4) | 0) { if (data[i] !== 0) { return false; } } return true; } function isColEmpty({ data, width }: PixelData, x: number, top: number, bottom: number) { const stride = (width * 4) | 0; const start = (top * stride + x * 4 + 3) | 0; for (let y = top, i = start; y < bottom; y++, i = (i + stride) | 0) { if (data[i] !== 0) { return false; } } return true; } function trimData(data: PixelData) { let top = 0; let left = 0; let right = data.width; let bottom = data.height; while (top < bottom && isRowEmpty(data, top, left, right)) top++; while (bottom > top && isRowEmpty(data, bottom - 1, left, right)) bottom--; while (left < right && isColEmpty(data, left, top, bottom)) left++; while (right > left && isColEmpty(data, right - 1, top, bottom)) right--; return { top, left, right, bottom }; } export function writeColor(writer: PsdWriter, color: Color | undefined) { if (!color) { writeUint16(writer, ColorSpace.RGB); writeZeros(writer, 8); } else if ('r' in color) { writeUint16(writer, ColorSpace.RGB); writeUint16(writer, Math.round(color.r * 257)); writeUint16(writer, Math.round(color.g * 257)); writeUint16(writer, Math.round(color.b * 257)); writeUint16(writer, 0); } else if ('fr' in color) { writeUint16(writer, ColorSpace.RGB); writeUint16(writer, Math.round(color.fr * 255 * 257)); writeUint16(writer, Math.round(color.fg * 255 * 257)); writeUint16(writer, Math.round(color.fb * 255 * 257)); writeUint16(writer, 0); } else if ('l' in color) { writeUint16(writer, ColorSpace.Lab); writeInt16(writer, Math.round(color.l * 10000)); writeInt16(writer, Math.round(color.a < 0 ? (color.a * 12800) : (color.a * 12700))); writeInt16(writer, Math.round(color.b < 0 ? (color.b * 12800) : (color.b * 12700))); writeUint16(writer, 0); } else if ('h' in color) { writeUint16(writer, ColorSpace.HSB); writeUint16(writer, Math.round(color.h * 0xffff)); writeUint16(writer, Math.round(color.s * 0xffff)); writeUint16(writer, Math.round(color.b * 0xffff)); writeUint16(writer, 0); } else if ('c' in color) { writeUint16(writer, ColorSpace.CMYK); writeUint16(writer, Math.round(color.c * 257)); writeUint16(writer, Math.round(color.m * 257)); writeUint16(writer, Math.round(color.y * 257)); writeUint16(writer, Math.round(color.k * 257)); } else { writeUint16(writer, ColorSpace.Grayscale); writeUint16(writer, Math.round(color.k * 10000 / 255)); writeZeros(writer, 6); } }