UNPKG

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
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