image-in-browser
Version:
Package for encoding / decoding images, transforming images, applying filters, drawing primitives on images on the client side (no need for server Node.js)
544 lines • 19.7 kB
JavaScript
import { ColorUtils } from '../../color/color-utils.js';
import { InputBuffer } from '../../common/input-buffer.js';
import { MathUtils } from '../../common/math-utils.js';
import { LibError } from '../../error/lib-error.js';
import { MemoryImage } from '../../image/image.js';
import { PsdBlendMode } from './psd-blend-mode.js';
import { PsdChannel } from './psd-channel.js';
import { PsdColorMode } from './psd-color-mode.js';
import { PsdImageResource } from './psd-image-resource.js';
import { PsdLayer } from './psd-layer.js';
export class PsdImage {
get input() {
return this._input;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
get backgroundColor() {
return this._backgroundColor;
}
get numFrames() {
return this._numFrames;
}
get signature() {
return this._signature;
}
get version() {
return this._version;
}
get channels() {
return this._channels;
}
get depth() {
return this._depth;
}
get colorMode() {
return this._colorMode;
}
get layers() {
return this._layers;
}
get mergeImageChannels() {
return this._mergeImageChannels;
}
get mergedImage() {
return this._mergedImage;
}
get imageResources() {
return this._imageResources;
}
get hasAlpha() {
return this._hasAlpha;
}
get isValid() {
return this._signature === PsdImage.psdSignature;
}
constructor(bytes) {
this._width = 0;
this._height = 0;
this._backgroundColor = undefined;
this._numFrames = 1;
this._imageResources = new Map();
this._hasAlpha = false;
this._input = new InputBuffer({
buffer: bytes,
bigEndian: true,
});
this.readHeader();
if (!this.isValid) {
return;
}
let len = this._input.readUint32();
this._input.readRange(len);
len = this._input.readUint32();
this._imageResourceData = this._input.readRange(len);
len = this._input.readUint32();
this._layerAndMaskData = this._input.readRange(len);
this._imageData = this._input.readRange(this._input.length);
}
static blendLighten(a, b) {
return Math.max(a, b);
}
static blendDarken(a, b) {
return Math.min(a, b);
}
static blendMultiply(a, b) {
return (a * b) >> 8;
}
static blendOverlay(a, b, aAlpha, bAlpha) {
const x = a / 255;
const y = b / 255;
const aa = aAlpha / 255;
const ba = bAlpha / 255;
let z = 0;
if (2 * x < aa) {
z = 2 * y * x + y * (1 - aa) + x * (1 - ba);
}
else {
z = ba * aa - 2 * (aa - x) * (ba - y) + y * (1 - aa) + x * (1 - ba);
}
return MathUtils.clampInt255(z * 255);
}
static blendColorBurn(a, b) {
if (b === 0) {
return 0;
}
const c = Math.trunc(255 * (1 - (1 - a / 255) / (b / 255)));
return MathUtils.clampInt255(c);
}
static blendLinearBurn(a, b) {
return MathUtils.clampInt255(a + b - 255);
}
static blendScreen(a, b) {
return MathUtils.clampInt255(255 - (255 - b) * (255 - a));
}
static blendColorDodge(a, b) {
if (b === 255) {
return 255;
}
return MathUtils.clampInt255((a / 255 / (1 - b / 255)) * 255);
}
static blendLinearDodge(a, b) {
return b + a > 255 ? 0xff : a + b;
}
static blendSoftLight(a, b) {
const aa = a / 255;
const bb = b / 255;
return Math.round(255 * ((1 - bb) * bb * aa + bb * (1 - (1 - bb) * (1 - aa))));
}
static blendHardLight(bottom, top) {
const a = top / 255;
const b = bottom / 255;
if (b < 0.5) {
return Math.round(255 * 2 * a * b);
}
else {
return Math.round(255 * (1 - 2 * (1 - a) * (1 - b)));
}
}
static blendVividLight(bottom, top) {
if (top < 128) {
return this.blendColorBurn(bottom, 2 * top);
}
else {
return this.blendColorDodge(bottom, 2 * (top - 128));
}
}
static blendLinearLight(bottom, top) {
if (top < 128) {
return this.blendLinearBurn(bottom, 2 * top);
}
else {
return this.blendLinearDodge(bottom, 2 * (top - 128));
}
}
static blendPinLight(bottom, top) {
return top < 128
? this.blendDarken(bottom, 2 * top)
: this.blendLighten(bottom, 2 * (top - 128));
}
static blendHardMix(bottom, top) {
return top < 255 - bottom ? 0 : 255;
}
static blendDifference(bottom, top) {
return Math.abs(top - bottom);
}
static blendExclusion(bottom, top) {
return Math.round(top + bottom - (2 * top * bottom) / 255);
}
static ch(data, si, ns) {
return data === undefined
? 0
: ns === 1
? data[si]
: ((data[si] << 8) | data[si + 1]) >> 8;
}
static createImageFromChannels(width, height, channelList, colorMode, bitDepth) {
const channels = new Map();
for (const ch of channelList) {
channels.set(ch.id, ch);
}
const numChannels = channelList.length;
const ns = bitDepth === 8 ? 1 : bitDepth === 16 ? 2 : -1;
const output = new MemoryImage({
width: width,
height: height,
numChannels: numChannels,
});
if (ns === -1) {
throw new LibError(`PSD: unsupported bit depth: ${bitDepth}`);
}
const channel0 = channels.get(0);
const channel1 = channels.get(1);
const channel2 = channels.get(2);
const channel_1 = channels.get(-1);
const rgb = [0, 0, 0];
let si = -ns;
for (const p of output) {
si += ns;
switch (colorMode) {
case PsdColorMode.rgb: {
p.r = this.ch(channel0.data, si, ns);
p.g = this.ch(channel1.data, si, ns);
p.b = this.ch(channel2.data, si, ns);
p.a = numChannels >= 4 ? this.ch(channel_1.data, si, ns) : 255;
if (p.a !== 0) {
p.r = ((p.r + p.a - 255) * 255) / p.a;
p.g = ((p.g + p.a - 255) * 255) / p.a;
p.b = ((p.b + p.a - 255) * 255) / p.a;
}
break;
}
case PsdColorMode.lab: {
const L = (this.ch(channel0.data, si, ns) * 100) >> 8;
const a = this.ch(channel1.data, si, ns) - 128;
const b = this.ch(channel2.data, si, ns) - 128;
const alpha = numChannels >= 4 ? this.ch(channel_1.data, si, ns) : 255;
const rgb = ColorUtils.labToRgb(L, a, b);
p.r = rgb[0];
p.g = rgb[1];
p.b = rgb[2];
p.a = alpha;
break;
}
case PsdColorMode.grayscale: {
const gray = this.ch(channel0.data, si, ns);
const alpha = numChannels >= 2 ? this.ch(channel_1.data, si, ns) : 255;
p.r = gray;
p.g = gray;
p.b = gray;
p.a = alpha;
break;
}
case PsdColorMode.cmyk: {
const c = this.ch(channel0.data, si, ns);
const m = this.ch(channel1.data, si, ns);
const y = this.ch(channel2.data, si, ns);
const k = this.ch(channels.get(numChannels === 4 ? -1 : 3).data, si, ns);
const alpha = numChannels >= 5 ? this.ch(channel_1.data, si, ns) : 255;
ColorUtils.cmykToRgb(255 - c, 255 - m, 255 - y, 255 - k, rgb);
p.r = rgb[0];
p.g = rgb[1];
p.b = rgb[2];
p.a = alpha;
break;
}
default:
throw new LibError(`Unhandled color mode: ${colorMode}`);
}
}
return output;
}
blend(ar, ag, ab, aa, br, bg, bb, ba, blendMode, opacity, p) {
let r = br;
let g = bg;
let b = bb;
let a = ba;
const da = (ba / 255) * opacity;
switch (blendMode) {
case PsdBlendMode.passThrough:
r = ar;
g = ag;
b = ab;
a = aa;
break;
case PsdBlendMode.normal:
break;
case PsdBlendMode.dissolve:
break;
case PsdBlendMode.darken:
r = PsdImage.blendDarken(ar, br);
g = PsdImage.blendDarken(ag, bg);
b = PsdImage.blendDarken(ab, bb);
break;
case PsdBlendMode.multiply:
r = PsdImage.blendMultiply(ar, br);
g = PsdImage.blendMultiply(ag, bg);
b = PsdImage.blendMultiply(ab, bb);
break;
case PsdBlendMode.colorBurn:
r = PsdImage.blendColorBurn(ar, br);
g = PsdImage.blendColorBurn(ag, bg);
b = PsdImage.blendColorBurn(ab, bb);
break;
case PsdBlendMode.linearBurn:
r = PsdImage.blendLinearBurn(ar, br);
g = PsdImage.blendLinearBurn(ag, bg);
b = PsdImage.blendLinearBurn(ab, bb);
break;
case PsdBlendMode.darkenColor:
break;
case PsdBlendMode.lighten:
r = PsdImage.blendLighten(ar, br);
g = PsdImage.blendLighten(ag, bg);
b = PsdImage.blendLighten(ab, bb);
break;
case PsdBlendMode.screen:
r = PsdImage.blendScreen(ar, br);
g = PsdImage.blendScreen(ag, bg);
b = PsdImage.blendScreen(ab, bb);
break;
case PsdBlendMode.colorDodge:
r = PsdImage.blendColorDodge(ar, br);
g = PsdImage.blendColorDodge(ag, bg);
b = PsdImage.blendColorDodge(ab, bb);
break;
case PsdBlendMode.linearDodge:
r = PsdImage.blendLinearDodge(ar, br);
g = PsdImage.blendLinearDodge(ag, bg);
b = PsdImage.blendLinearDodge(ab, bb);
break;
case PsdBlendMode.lighterColor:
break;
case PsdBlendMode.overlay:
r = PsdImage.blendOverlay(ar, br, aa, ba);
g = PsdImage.blendOverlay(ag, bg, aa, ba);
b = PsdImage.blendOverlay(ab, bb, aa, ba);
break;
case PsdBlendMode.softLight:
r = PsdImage.blendSoftLight(ar, br);
g = PsdImage.blendSoftLight(ag, bg);
b = PsdImage.blendSoftLight(ab, bb);
break;
case PsdBlendMode.hardLight:
r = PsdImage.blendHardLight(ar, br);
g = PsdImage.blendHardLight(ag, bg);
b = PsdImage.blendHardLight(ab, bb);
break;
case PsdBlendMode.vividLight:
r = PsdImage.blendVividLight(ar, br);
g = PsdImage.blendVividLight(ag, bg);
b = PsdImage.blendVividLight(ab, bb);
break;
case PsdBlendMode.linearLight:
r = PsdImage.blendLinearLight(ar, br);
g = PsdImage.blendLinearLight(ag, bg);
b = PsdImage.blendLinearLight(ab, bb);
break;
case PsdBlendMode.pinLight:
r = PsdImage.blendPinLight(ar, br);
g = PsdImage.blendPinLight(ag, bg);
b = PsdImage.blendPinLight(ab, bb);
break;
case PsdBlendMode.hardMix:
r = PsdImage.blendHardMix(ar, br);
g = PsdImage.blendHardMix(ag, bg);
b = PsdImage.blendHardMix(ab, bb);
break;
case PsdBlendMode.difference:
r = PsdImage.blendDifference(ar, br);
g = PsdImage.blendDifference(ag, bg);
b = PsdImage.blendDifference(ab, bb);
break;
case PsdBlendMode.exclusion:
r = PsdImage.blendExclusion(ar, br);
g = PsdImage.blendExclusion(ag, bg);
b = PsdImage.blendExclusion(ab, bb);
break;
case PsdBlendMode.subtract:
break;
case PsdBlendMode.divide:
break;
case PsdBlendMode.hue:
break;
case PsdBlendMode.saturation:
break;
case PsdBlendMode.color:
break;
case PsdBlendMode.luminosity:
break;
}
p.r = Math.trunc(ar * (1 - da) + r * da);
p.g = Math.trunc(ag * (1 - da) + g * da);
p.b = Math.trunc(ab * (1 - da) + b * da);
p.a = Math.trunc(aa * (1 - da) + a * da);
}
readHeader() {
this._signature = this._input.readUint32();
this._version = this._input.readUint16();
if (this.version !== 1) {
this._signature = 0;
return;
}
const padding = this._input.readRange(6);
for (let i = 0; i < 6; ++i) {
if (padding.get(i) !== 0) {
this._signature = 0;
return;
}
}
this._channels = this._input.readUint16();
this._height = this._input.readUint32();
this._width = this._input.readUint32();
this._depth = this._input.readUint16();
this._colorMode = this._input.readUint16();
}
readColorModeData() { }
readImageResources() {
this._imageResourceData.rewind();
while (!this._imageResourceData.isEOS) {
const blockSignature = this._imageResourceData.readUint32();
const blockId = this._imageResourceData.readUint16();
let len = this._imageResourceData.read();
const blockName = this._imageResourceData.readString(len);
if ((len & 1) === 0) {
this._imageResourceData.skip(1);
}
len = this._imageResourceData.readUint32();
const blockData = this._imageResourceData.readRange(len);
if ((len & 1) === 1) {
this._imageResourceData.skip(1);
}
if (blockSignature === PsdImage.resourceBlockSignature) {
this._imageResources.set(blockId, new PsdImageResource(blockId, blockName, blockData));
}
}
}
readLayerAndMaskData() {
this._layerAndMaskData.rewind();
let len = this._layerAndMaskData.readUint32();
if ((len & 1) !== 0) {
len++;
}
const layerData = this._layerAndMaskData.readRange(len);
this._layers = [];
if (len > 0) {
let count = layerData.readInt16();
if (count < 0) {
this._hasAlpha = true;
count = -count;
}
for (let i = 0; i < count; ++i) {
const layer = new PsdLayer(layerData);
this._layers.push(layer);
}
}
for (let i = 0; i < this._layers.length; ++i) {
this._layers[i].readImageData(layerData, this);
}
len = this._layerAndMaskData.readUint32();
const maskData = this._layerAndMaskData.readRange(len);
if (len > 0) {
maskData.readUint16();
maskData.readUint16();
maskData.readUint16();
maskData.readUint16();
maskData.readUint16();
maskData.readUint16();
maskData.read();
}
}
readMergeImageData() {
this._imageData.rewind();
const compression = this._imageData.readUint16();
let lineLengths = undefined;
if (compression === PsdChannel.compressRle) {
const numLines = this._height * this._channels;
lineLengths = new Uint16Array(numLines);
for (let i = 0; i < numLines; ++i) {
lineLengths[i] = this._imageData.readUint16();
}
}
this._mergeImageChannels = [];
for (let i = 0; i < this._channels; ++i) {
this._mergeImageChannels.push(PsdChannel.read({
input: this._imageData,
id: i === 3 ? -1 : i,
width: this._width,
height: this._height,
bitDepth: this._depth,
compression: compression,
planeNumber: i,
lineLengths: lineLengths,
}));
}
this._mergedImage = PsdImage.createImageFromChannels(this._width, this._height, this._mergeImageChannels, this._colorMode, this._depth);
}
decode() {
if (!this.isValid || this._input === undefined) {
return false;
}
this.readColorModeData();
this.readImageResources();
this.readLayerAndMaskData();
this.readMergeImageData();
this._input = undefined;
this._imageResourceData = undefined;
this._layerAndMaskData = undefined;
this._imageData = undefined;
return true;
}
decodeImage() {
if (!this.decode()) {
return undefined;
}
return this.renderImage();
}
renderImage() {
if (this._mergedImage !== undefined) {
return this._mergedImage;
}
this._mergedImage = new MemoryImage({
width: this._width,
height: this._height,
numChannels: 4,
});
this._mergedImage.clear();
for (let li = 0; li < this._layers.length; ++li) {
const layer = this._layers[li];
if (!layer.isVisible) {
continue;
}
const opacity = layer.opacity / 255;
const blendMode = layer.blendMode;
const src = layer.layerImage;
for (let y = 0, sy = layer.top; y < layer.height; ++y, ++sy) {
const dy = layer.top + y;
for (let x = 0, sx = layer.left; x < layer.width; ++x, ++sx) {
const srcP = src.getPixel(x, y);
const br = Math.trunc(srcP.r);
const bg = Math.trunc(srcP.g);
const bb = Math.trunc(srcP.b);
const ba = Math.trunc(srcP.a);
if (sx >= 0 && sx < this._width && sy >= 0 && sy < this._height) {
const dx = layer.left + x;
const p = this._mergedImage.getPixel(dx, dy);
const ar = Math.trunc(p.r);
const ag = Math.trunc(p.g);
const ab = Math.trunc(p.b);
const aa = Math.trunc(p.a);
this.blend(ar, ag, ab, aa, br, bg, bb, ba, blendMode, opacity, p);
}
}
}
}
return this._mergedImage;
}
}
PsdImage.psdSignature = 0x38425053;
PsdImage.resourceBlockSignature = 0x3842494d;
//# sourceMappingURL=psd-image.js.map