mirror of
https://gitee.com/onvia/ccc-tnt-psd2ui
synced 2025-11-02 20:36:15 +00:00
388 lines
9.4 KiB
TypeScript
388 lines
9.4 KiB
TypeScript
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<T>(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;
|
|
}
|