import { fromByteArray } from 'base64-js'; import { deflate } from 'pako'; import { Layer, BlendMode, LayerColor } from './psd'; export const MOCK_HANDLERS = false; export const RAW_IMAGE_DATA = false; export const fromBlendMode: { [key: string]: string } = {}; export const toBlendMode: { [key: string]: BlendMode } = { 'pass': 'pass through', 'norm': 'normal', 'diss': 'dissolve', 'dark': 'darken', 'mul ': 'multiply', 'idiv': 'color burn', 'lbrn': 'linear burn', 'dkCl': 'darker color', 'lite': 'lighten', 'scrn': 'screen', 'div ': 'color dodge', 'lddg': 'linear dodge', 'lgCl': 'lighter color', 'over': 'overlay', 'sLit': 'soft light', 'hLit': 'hard light', 'vLit': 'vivid light', 'lLit': 'linear light', 'pLit': 'pin light', 'hMix': 'hard mix', 'diff': 'difference', 'smud': 'exclusion', 'fsub': 'subtract', 'fdiv': 'divide', 'hue ': 'hue', 'sat ': 'saturation', 'colr': 'color', 'lum ': 'luminosity', }; Object.keys(toBlendMode).forEach(key => fromBlendMode[toBlendMode[key]] = key); export const layerColors: LayerColor[] = [ 'none', 'red', 'orange', 'yellow', 'green', 'blue', 'violet', 'gray' ]; export const largeAdditionalInfoKeys = [ // from documentation 'LMsk', 'Lr16', 'Lr32', 'Layr', 'Mt16', 'Mt32', 'Mtrn', 'Alph', 'FMsk', 'lnk2', 'FEid', 'FXid', 'PxSD', // from guessing 'cinf', ]; export interface Dict { [key: string]: string; } export function revMap(map: Dict) { const result: Dict = {}; Object.keys(map).forEach(key => result[map[key]] = key); return result; } export function createEnum(prefix: string, def: string, map: Dict) { const rev = revMap(map); const decode = (val: string): T => { const value = val.split('.')[1]; if (value && !rev[value]) throw new Error(`Unrecognized value for enum: '${val}'`); return (rev[value] as any) || def; }; const encode = (val: T | undefined): string => { if (val && !map[val as any]) throw new Error(`Invalid value for enum: '${val}'`); return `${prefix}.${map[val as any] || map[def]}`; }; return { decode, encode }; } export const enum ColorSpace { RGB = 0, HSB = 1, CMYK = 2, Lab = 7, Grayscale = 8, } export const enum LayerMaskFlags { PositionRelativeToLayer = 1, LayerMaskDisabled = 2, InvertLayerMaskWhenBlending = 4, // obsolete LayerMaskFromRenderingOtherData = 8, MaskHasParametersAppliedToIt = 16, } export const enum MaskParams { UserMaskDensity = 1, UserMaskFeather = 2, VectorMaskDensity = 4, VectorMaskFeather = 8, } export const enum ChannelID { Color0 = 0, // red (rgb) / cyan (cmyk) Color1 = 1, // green (rgb) / magenta (cmyk) Color2 = 2, // blue (rgb) / yellow (cmyk) Color3 = 3, // - (rgb) / black (cmyk) Transparency = -1, UserMask = -2, RealUserMask = -3, } export const enum Compression { RawData = 0, RleCompressed = 1, ZipWithoutPrediction = 2, ZipWithPrediction = 3, } export interface ChannelData { channelId: ChannelID; compression: Compression; buffer: Uint8Array | undefined; length: number; } export interface Bounds { top: number; left: number; right: number; bottom: number; } export interface LayerChannelData { layer: Layer; channels: ChannelData[]; top: number; left: number; right: number; bottom: number; mask?: Bounds; } export type PixelArray = Uint8ClampedArray | Uint8Array; export interface PixelData { data: PixelArray; width: number; height: number; } export function offsetForChannel(channelId: ChannelID, cmyk: boolean) { switch (channelId) { case ChannelID.Color0: return 0; case ChannelID.Color1: return 1; case ChannelID.Color2: return 2; case ChannelID.Color3: return cmyk ? 3 : channelId + 1; case ChannelID.Transparency: return cmyk ? 4 : 3; default: return channelId + 1; } } export function clamp(value: number, min: number, max: number) { return value < min ? min : (value > max ? max : value); } export function hasAlpha(data: PixelData) { const size = data.width * data.height * 4; for (let i = 3; i < size; i += 4) { if (data.data[i] !== 255) { return true; } } return false; } export function resetImageData({ data }: PixelData) { const buffer = new Uint32Array(data.buffer); const size = buffer.length | 0; for (let p = 0; p < size; p = (p + 1) | 0) { buffer[p] = 0xff000000; } } export function decodeBitmap(input: PixelArray, output: PixelArray, width: number, height: number) { for (let y = 0, p = 0, o = 0; y < height; y++) { for (let x = 0; x < width;) { let b = input[o++]; for (let i = 0; i < 8 && x < width; i++, x++) { const v = b & 0x80 ? 0 : 255; b = b << 1; output[p++] = v; output[p++] = v; output[p++] = v; output[p++] = 255; } } } } export function writeDataRaw(data: PixelData, offset: number, width: number, height: number) { if (!width || !height) return undefined; const array = new Uint8Array(width * height); for (let i = 0; i < array.length; i++) { array[i] = data.data[i * 4 + offset]; } return array; } export function writeDataRLE(buffer: Uint8Array, { data, width, height }: PixelData, offsets: number[], large: boolean) { if (!width || !height) return undefined; const stride = (4 * width) | 0; let ol = 0; let o = (offsets.length * (large ? 4 : 2) * height) | 0; for (const offset of offsets) { for (let y = 0, p = offset | 0; y < height; y++) { const strideStart = (y * stride) | 0; const strideEnd = (strideStart + stride) | 0; const lastIndex = (strideEnd + offset - 4) | 0; const lastIndex2 = (lastIndex - 4) | 0; const startOffset = o; for (p = (strideStart + offset) | 0; p < strideEnd; p = (p + 4) | 0) { if (p < lastIndex2) { let value1 = data[p]; p = (p + 4) | 0; let value2 = data[p]; p = (p + 4) | 0; let value3 = data[p]; if (value1 === value2 && value1 === value3) { let count = 3; while (count < 128 && p < lastIndex && data[(p + 4) | 0] === value1) { count = (count + 1) | 0; p = (p + 4) | 0; } buffer[o++] = 1 - count; buffer[o++] = value1; } else { const countIndex = o; let writeLast = true; let count = 1; buffer[o++] = 0; buffer[o++] = value1; while (p < lastIndex && count < 128) { p = (p + 4) | 0; value1 = value2; value2 = value3; value3 = data[p]; if (value1 === value2 && value1 === value3) { p = (p - 12) | 0; writeLast = false; break; } else { count++; buffer[o++] = value1; } } if (writeLast) { if (count < 127) { buffer[o++] = value2; buffer[o++] = value3; count += 2; } else if (count < 128) { buffer[o++] = value2; count++; p = (p - 4) | 0; } else { p = (p - 8) | 0; } } buffer[countIndex] = count - 1; } } else if (p === lastIndex) { buffer[o++] = 0; buffer[o++] = data[p]; } else { // p === lastIndex2 buffer[o++] = 1; buffer[o++] = data[p]; p = (p + 4) | 0; buffer[o++] = data[p]; } } const length = o - startOffset; if (large) { buffer[ol++] = (length >> 24) & 0xff; buffer[ol++] = (length >> 16) & 0xff; } buffer[ol++] = (length >> 8) & 0xff; buffer[ol++] = length & 0xff; } } return buffer.slice(0, o); } export function writeDataZipWithoutPrediction({ data, width, height }: PixelData, offsets: number[]) { const size = width * height; const channel = new Uint8Array(size); const buffers: Uint8Array[] = []; let totalLength = 0; for (const offset of offsets) { for (let i = 0, o = offset; i < size; i++, o += 4) { channel[i] = data[o]; } const buffer = deflate(channel); buffers.push(buffer); totalLength += buffer.byteLength; } if (buffers.length > 0) { const buffer = new Uint8Array(totalLength); let offset = 0; for (const b of buffers) { buffer.set(b, offset); offset += b.byteLength; } return buffer; } else { return buffers[0]; } } export let createCanvas: (width: number, height: number) => HTMLCanvasElement = () => { throw new Error('Canvas not initialized, use initializeCanvas method to set up createCanvas method'); }; export let createCanvasFromData: (data: Uint8Array) => HTMLCanvasElement = () => { throw new Error('Canvas not initialized, use initializeCanvas method to set up createCanvasFromData method'); }; let tempCanvas: HTMLCanvasElement | undefined = undefined; export let createImageData: (width: number, height: number) => ImageData = (width, height) => { if (!tempCanvas) tempCanvas = createCanvas(1, 1); return tempCanvas.getContext('2d')!.createImageData(width, height); }; if (typeof document !== 'undefined') { createCanvas = (width, height) => { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; }; createCanvasFromData = (data) => { const image = new Image(); image.src = 'data:image/jpeg;base64,' + fromByteArray(data); const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; canvas.getContext('2d')!.drawImage(image, 0, 0); return canvas; }; } export function initializeCanvas( createCanvasMethod: (width: number, height: number) => HTMLCanvasElement, createCanvasFromDataMethod?: (data: Uint8Array) => HTMLCanvasElement, createImageDataMethod?: (width: number, height: number) => ImageData ) { createCanvas = createCanvasMethod; createCanvasFromData = createCanvasFromDataMethod || createCanvasFromData; createImageData = createImageDataMethod || createImageData; }