mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-17 12:22:21 +02:00
feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).
Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c74f475679
commit
5fac693c1c
@@ -0,0 +1,497 @@
|
||||
/** Minimal Adobe Photoshop (PSD) reader/writer.
|
||||
*
|
||||
* Exports a flattened 8-bit RGB composite as a valid `8BPS` document with
|
||||
* no layer records, and parses the same shape back. Also decodes RLE
|
||||
* (PackBits) image data, which is what Photoshop normally writes.
|
||||
*/
|
||||
|
||||
const SIGNATURE = 0x38425053; // '8BPS'
|
||||
const COLOR_MODE_RGB = 3;
|
||||
const COLOR_MODE_GRAYSCALE = 1;
|
||||
const COMPRESSION_RAW = 0;
|
||||
const COMPRESSION_RLE = 1;
|
||||
|
||||
function writeUint16(view, offset, value) {
|
||||
view.setUint16(offset, value & 0xffff, false);
|
||||
}
|
||||
|
||||
function writeUint32(view, offset, value) {
|
||||
view.setUint32(offset, value >>> 0, false);
|
||||
}
|
||||
|
||||
function writeUint64(view, offset, value) {
|
||||
view.setBigUint64(offset, BigInt(value), false);
|
||||
}
|
||||
|
||||
/** PackBits (RLE) encode a byte array. */
|
||||
export function packBits(input) {
|
||||
const out = [];
|
||||
const length = input.length;
|
||||
let i = 0;
|
||||
while (i < length) {
|
||||
let runLength = 1;
|
||||
while (i + runLength < length && runLength < 128 && input[i + runLength] === input[i]) runLength++;
|
||||
if (runLength >= 2) {
|
||||
out.push((256 - (runLength - 1)) & 0xff, input[i]);
|
||||
i += runLength;
|
||||
continue;
|
||||
}
|
||||
let literalStart = i;
|
||||
let literalLength = 0;
|
||||
while (i < length && literalLength < 128) {
|
||||
let nextRun = 1;
|
||||
while (i + nextRun < length && nextRun < 128 && input[i + nextRun] === input[i]) nextRun++;
|
||||
if (nextRun >= 2 && literalLength > 0) break;
|
||||
i += 1;
|
||||
literalLength += 1;
|
||||
if (i >= length) break;
|
||||
}
|
||||
if (i === literalStart) i += 1;
|
||||
const count = i - literalStart;
|
||||
out.push(count - 1, ...input.slice(literalStart, i));
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
|
||||
/** PackBits (RLE) decode `length` bytes starting at `offset`. */
|
||||
export function unpackBits(data, offset, length) {
|
||||
const out = new Uint8Array(length);
|
||||
let outPos = 0;
|
||||
let pos = offset;
|
||||
while (outPos < length && pos < data.length) {
|
||||
const header = data[pos] < 128 ? data[pos] : data[pos] - 256;
|
||||
pos += 1;
|
||||
if (header >= 0) {
|
||||
const count = header + 1;
|
||||
for (let i = 0; i < count && outPos < length; i++) out[outPos++] = data[pos + i];
|
||||
pos += count;
|
||||
} else if (header !== -128) {
|
||||
const count = 1 - header;
|
||||
const value = data[pos++];
|
||||
for (let i = 0; i < count && outPos < length; i++) out[outPos++] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Serialize a canvas as a flattened 8-bit RGB PSD. */
|
||||
export function exportPsd(sourceCanvas) {
|
||||
const width = sourceCanvas.width;
|
||||
const height = sourceCanvas.height;
|
||||
const pixels = sourceCanvas.getContext('2d').getImageData(0, 0, width, height).data;
|
||||
const pixelCount = width * height;
|
||||
const red = new Uint8Array(pixelCount);
|
||||
const green = new Uint8Array(pixelCount);
|
||||
const blue = new Uint8Array(pixelCount);
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
red[i] = pixels[i * 4];
|
||||
green[i] = pixels[i * 4 + 1];
|
||||
blue[i] = pixels[i * 4 + 2];
|
||||
}
|
||||
const channels = [red, green, blue];
|
||||
const rowLengths = [];
|
||||
const rows = [];
|
||||
for (const channel of channels) {
|
||||
let channelLength = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = packBits(channel.subarray(y * width, (y + 1) * width));
|
||||
rowLengths.push(row.length);
|
||||
rows.push(row);
|
||||
channelLength += row.length;
|
||||
}
|
||||
}
|
||||
const rleTableBytes = rowLengths.length * 2;
|
||||
const rleBodyBytes = rows.reduce((sum, row) => sum + row.length, 0);
|
||||
const headerBytes = 26;
|
||||
const imageDataBytes = 2 + rleTableBytes + rleBodyBytes;
|
||||
const total = headerBytes + 4 * 3 + imageDataBytes;
|
||||
const buffer = new ArrayBuffer(total);
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
writeUint32(view, 0, SIGNATURE);
|
||||
writeUint16(view, 4, 1);
|
||||
offset = 10; // reserved 6 bytes
|
||||
writeUint16(view, offset, 3); offset += 2;
|
||||
writeUint32(view, offset, height); offset += 4;
|
||||
writeUint32(view, offset, width); offset += 4;
|
||||
writeUint16(view, offset, 8); offset += 2;
|
||||
writeUint16(view, offset, COLOR_MODE_RGB); offset += 2;
|
||||
// Color mode data + image resources + layer/mask sections are empty.
|
||||
writeUint32(view, offset, 0); offset += 4;
|
||||
writeUint32(view, offset, 0); offset += 4;
|
||||
writeUint32(view, offset, 0); offset += 4;
|
||||
writeUint16(view, offset, COMPRESSION_RLE); offset += 2;
|
||||
for (const length of rowLengths) {
|
||||
writeUint16(view, offset, length); offset += 2;
|
||||
}
|
||||
for (const row of rows) {
|
||||
bytes.set(row, offset);
|
||||
offset += row.length;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function readSection(view, bytes, offset, long = false) {
|
||||
if (long) {
|
||||
const length = Number(view.getBigUint64(offset, false));
|
||||
return offset + 8 + length;
|
||||
}
|
||||
const length = view.getUint32(offset, false);
|
||||
return offset + 4 + length;
|
||||
}
|
||||
|
||||
/** Parse an 8BPS buffer into `{ width, height, canvas }`. */
|
||||
export function parsePsd(arrayBuffer) {
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const view = new DataView(arrayBuffer);
|
||||
if (view.getUint32(0, false) !== SIGNATURE) throw new Error('Not a PSD file');
|
||||
const version = view.getUint16(4, false);
|
||||
if (version !== 1 && version !== 2) throw new Error('Unsupported PSD version');
|
||||
const psb = version === 2;
|
||||
const channels = view.getUint16(12, false);
|
||||
const height = view.getUint32(14, false);
|
||||
const width = view.getUint32(18, false);
|
||||
const depth = view.getUint16(22, false);
|
||||
const colorMode = view.getUint16(24, false);
|
||||
if (depth !== 8) throw new Error('Unsupported PSD bit depth');
|
||||
if (colorMode !== COLOR_MODE_RGB && colorMode !== COLOR_MODE_GRAYSCALE) {
|
||||
throw new Error('Unsupported PSD color mode');
|
||||
}
|
||||
let offset = 26;
|
||||
offset = readSection(view, bytes, offset); // color mode data (4-byte length)
|
||||
offset = readSection(view, bytes, offset); // image resources (4-byte length)
|
||||
offset = readSection(view, bytes, offset, psb); // layer & mask info (8-byte length in PSB)
|
||||
const compression = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
const pixelCount = width * height;
|
||||
const planes = [];
|
||||
if (compression === COMPRESSION_RAW) {
|
||||
for (let c = 0; c < channels; c++) {
|
||||
planes.push(bytes.subarray(offset, offset + pixelCount));
|
||||
offset += pixelCount;
|
||||
}
|
||||
} else if (compression === COMPRESSION_RLE) {
|
||||
const rowCount = channels * height;
|
||||
const rowLengths = [];
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
rowLengths.push(psb ? view.getUint32(offset, false) : view.getUint16(offset, false));
|
||||
offset += psb ? 4 : 2;
|
||||
}
|
||||
for (let c = 0; c < channels; c++) {
|
||||
const plane = new Uint8Array(pixelCount);
|
||||
for (let y = 0; y < height; y++) {
|
||||
const rowIndex = c * height + y;
|
||||
const decoded = unpackBits(bytes, offset, rowLengths[rowIndex]);
|
||||
plane.set(decoded.subarray(0, width), y * width);
|
||||
offset += rowLengths[rowIndex];
|
||||
}
|
||||
planes.push(plane);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported PSD compression');
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const image = ctx.createImageData(width, height);
|
||||
const isGray = colorMode === COLOR_MODE_GRAYSCALE;
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const r = planes[0] ? planes[0][i] : 0;
|
||||
const g = isGray ? r : (planes[1] ? planes[1][i] : 0);
|
||||
const b = isGray ? r : (planes[2] ? planes[2][i] : 0);
|
||||
image.data[i * 4] = r;
|
||||
image.data[i * 4 + 1] = g;
|
||||
image.data[i * 4 + 2] = b;
|
||||
image.data[i * 4 + 3] = 255;
|
||||
}
|
||||
ctx.putImageData(image, 0, 0);
|
||||
return { width, height, canvas };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layered PSD (8BPS v1, 8-bit RGB) — layer records + raw channel planes.
|
||||
// Encoder/decoder operate on plain RGBA arrays (no canvas) so they are
|
||||
// usable and testable outside the browser.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function rgbaToPlanes(data, width, height) {
|
||||
const count = width * height;
|
||||
const r = new Uint8Array(count);
|
||||
const g = new Uint8Array(count);
|
||||
const b = new Uint8Array(count);
|
||||
const a = new Uint8Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
r[i] = data[i * 4];
|
||||
g[i] = data[i * 4 + 1];
|
||||
b[i] = data[i * 4 + 2];
|
||||
a[i] = data[i * 4 + 3];
|
||||
}
|
||||
return [r, g, b, a];
|
||||
}
|
||||
|
||||
function blendRgbaInto(target, layer, docWidth, docHeight) {
|
||||
const lw = layer.width;
|
||||
const lh = layer.height;
|
||||
const ox = layer.x | 0;
|
||||
const oy = layer.y | 0;
|
||||
const alpha = layer.opacity == null ? 1 : layer.opacity;
|
||||
const data = layer.data;
|
||||
for (let y = 0; y < lh; y++) {
|
||||
const ty = oy + y;
|
||||
if (ty < 0 || ty >= docHeight) continue;
|
||||
for (let x = 0; x < lw; x++) {
|
||||
const tx = ox + x;
|
||||
if (tx < 0 || tx >= docWidth) continue;
|
||||
const si = (y * lw + x) * 4;
|
||||
const ti = (ty * docWidth + tx) * 4;
|
||||
const sa = (data[si + 3] / 255) * alpha;
|
||||
if (sa <= 0) continue;
|
||||
const da = target[ti + 3] / 255;
|
||||
const outA = sa + da * (1 - sa);
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const sc = data[si + c];
|
||||
const dc = target[ti + c];
|
||||
target[ti + c] = outA ? Math.round((sc * sa + dc * da * (1 - sa)) / outA) : 0;
|
||||
}
|
||||
target[ti + 3] = Math.round(outA * 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function latin1Bytes(text) {
|
||||
const out = [];
|
||||
for (const char of String(text)) out.push(char.charCodeAt(0) & 0xff);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Serialize layered RGBA layers + a flattened composite as an 8BPS PSD
|
||||
* (version 1) or PSB (version 2, "large document format", 8-byte lengths). */
|
||||
export function encodeLayeredPsd({ width, height, layers = [], psb = false } = {}) {
|
||||
const longLen = psb ? 8 : 4;
|
||||
const composite = new Uint8ClampedArray(width * height * 4);
|
||||
const ordered = layers.slice().reverse(); // PSD stores bottom-first
|
||||
for (const layer of ordered) {
|
||||
if (layer.visible === false) continue;
|
||||
blendRgbaInto(composite, layer, width, height);
|
||||
}
|
||||
|
||||
const records = [];
|
||||
const channelBlocks = [];
|
||||
for (const layer of ordered) {
|
||||
const lw = layer.width;
|
||||
const lh = layer.height;
|
||||
const planes = rgbaToPlanes(layer.data, lw, lh);
|
||||
const channelIds = [0, 1, 2, -1];
|
||||
const channelLength = 2 + lw * lh;
|
||||
const name = latin1Bytes(layer.name || 'Layer');
|
||||
const namePadded = 1 + name.length + ((4 - ((1 + name.length) % 4)) % 4);
|
||||
const extraLength = 4 + 4 + namePadded;
|
||||
|
||||
const record = new Uint8Array(4 * 4 + 2 + channelIds.length * (2 + longLen) + 4 + 4 + 4 + 4 + extraLength);
|
||||
const rv = new DataView(record.buffer);
|
||||
let o = 0;
|
||||
rv.setInt32(o, layer.y | 0, false); o += 4;
|
||||
rv.setInt32(o, layer.x | 0, false); o += 4;
|
||||
rv.setInt32(o, (layer.y | 0) + lh, false); o += 4;
|
||||
rv.setInt32(o, (layer.x | 0) + lw, false); o += 4;
|
||||
rv.setUint16(o, channelIds.length, false); o += 2;
|
||||
for (const id of channelIds) {
|
||||
rv.setInt16(o, id, false); o += 2;
|
||||
if (psb) { rv.setBigUint64(o, BigInt(channelLength), false); o += 8; }
|
||||
else { rv.setUint32(o, channelLength, false); o += 4; }
|
||||
}
|
||||
record.set([0x38, 0x42, 0x49, 0x4d], o); o += 4; // '8BIM'
|
||||
record.set(latin1Bytes((layer.blendKey || 'norm').padEnd(4, ' ').slice(0, 4)), o); o += 4;
|
||||
record[o++] = Math.round((layer.opacity == null ? 1 : layer.opacity) * 255);
|
||||
record[o++] = 0; // clipping
|
||||
record[o++] = layer.visible === false ? 2 : 0; // flags (bit1 = hidden)
|
||||
record[o++] = 0; // filler
|
||||
rv.setUint32(o, extraLength, false); o += 4;
|
||||
rv.setUint32(o, 0, false); o += 4; // layer mask data length
|
||||
rv.setUint32(o, 0, false); o += 4; // layer blending ranges length
|
||||
record[o++] = name.length;
|
||||
record.set(name, o); o += name.length;
|
||||
while (o < record.length) record[o++] = 0; // name padding
|
||||
records.push(record);
|
||||
|
||||
for (const plane of planes) {
|
||||
const block = new Uint8Array(2 + plane.length);
|
||||
block[0] = 0;
|
||||
block[1] = COMPRESSION_RAW;
|
||||
block.set(plane, 2);
|
||||
channelBlocks.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
const recordBytes = records.reduce((sum, record) => sum + record.length, 0);
|
||||
const channelBytes = channelBlocks.reduce((sum, block) => sum + block.length, 0);
|
||||
const layerInfoLength = 2 + recordBytes + channelBytes;
|
||||
|
||||
const layerInfo = new Uint8Array(longLen + layerInfoLength);
|
||||
const liv = new DataView(layerInfo.buffer);
|
||||
if (psb) liv.setBigUint64(0, BigInt(layerInfoLength), false); else liv.setUint32(0, layerInfoLength, false);
|
||||
liv.setInt16(longLen, ordered.length, false);
|
||||
let p = longLen + 2;
|
||||
for (const record of records) { layerInfo.set(record, p); p += record.length; }
|
||||
for (const block of channelBlocks) { layerInfo.set(block, p); p += block.length; }
|
||||
|
||||
const sectionDataLength = layerInfo.length + 4; // layerInfo blob + global mask (0)
|
||||
const section = new Uint8Array(longLen + sectionDataLength);
|
||||
const sv = new DataView(section.buffer);
|
||||
if (psb) sv.setBigUint64(0, BigInt(sectionDataLength), false); else sv.setUint32(0, sectionDataLength, false);
|
||||
section.set(layerInfo, longLen);
|
||||
sv.setUint32(longLen + layerInfo.length, 0, false); // global layer mask info length
|
||||
|
||||
// Composite image data (RLE), matching exportPsd.
|
||||
const compositePlanes = rgbaToPlanes(composite, width, height);
|
||||
const rowLengths = [];
|
||||
const rows = [];
|
||||
for (const plane of compositePlanes) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = packBits(plane.subarray(y * width, (y + 1) * width));
|
||||
rowLengths.push(row.length);
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
const rleTableBytes = rowLengths.length * (psb ? 4 : 2);
|
||||
const rleBodyBytes = rows.reduce((sum, row) => sum + row.length, 0);
|
||||
const imageDataBytes = 2 + rleTableBytes + rleBodyBytes;
|
||||
|
||||
const headerBytes = 26;
|
||||
const total = headerBytes + 4 + 4 + section.length + imageDataBytes;
|
||||
const buffer = new ArrayBuffer(total);
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const view = new DataView(buffer);
|
||||
writeUint32(view, 0, SIGNATURE);
|
||||
writeUint16(view, 4, psb ? 2 : 1);
|
||||
writeUint16(view, 12, 3); // channels
|
||||
writeUint32(view, 14, height);
|
||||
writeUint32(view, 18, width);
|
||||
writeUint16(view, 22, 8);
|
||||
writeUint16(view, 24, COLOR_MODE_RGB);
|
||||
writeUint32(view, 26, 0); // color mode data
|
||||
writeUint32(view, 30, 0); // image resources
|
||||
let offset = 34;
|
||||
bytes.set(section, offset); offset += section.length;
|
||||
writeUint16(view, offset, COMPRESSION_RLE); offset += 2;
|
||||
for (const length of rowLengths) {
|
||||
if (psb) { writeUint32(view, offset, length); offset += 4; }
|
||||
else { writeUint16(view, offset, length); offset += 2; }
|
||||
}
|
||||
for (const row of rows) { bytes.set(row, offset); offset += row.length; }
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** Decode a layered PSD into `{ width, height, layers }` (RGBA planes, no canvas). */
|
||||
export function decodeLayeredPsd(arrayBuffer) {
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const view = new DataView(arrayBuffer);
|
||||
if (view.getUint32(0, false) !== SIGNATURE) throw new Error('Not a PSD file');
|
||||
const version = view.getUint16(4, false);
|
||||
if (version !== 1 && version !== 2) throw new Error('Unsupported PSD version');
|
||||
const psb = version === 2;
|
||||
const longLen = psb ? 8 : 4;
|
||||
const height = view.getUint32(14, false);
|
||||
const width = view.getUint32(18, false);
|
||||
if (view.getUint16(22, false) !== 8) throw new Error('Unsupported PSD bit depth');
|
||||
let offset = 26;
|
||||
offset = readSection(view, bytes, offset); // color mode data (4-byte length)
|
||||
offset = readSection(view, bytes, offset); // image resources (4-byte length)
|
||||
const sectionLength = psb ? Number(view.getBigUint64(offset, false)) : view.getUint32(offset, false);
|
||||
offset += longLen;
|
||||
if (!sectionLength) return { width, height, layers: [] };
|
||||
const sectionEnd = offset + sectionLength;
|
||||
const layerInfoLength = psb ? Number(view.getBigUint64(offset, false)) : view.getUint32(offset, false);
|
||||
offset += longLen;
|
||||
const layerInfoEnd = offset + layerInfoLength;
|
||||
const rawCount = view.getInt16(offset, false);
|
||||
offset += 2;
|
||||
const layerCount = Math.abs(rawCount);
|
||||
const metas = [];
|
||||
for (let l = 0; l < layerCount; l++) {
|
||||
const top = view.getInt32(offset, false); offset += 4;
|
||||
const left = view.getInt32(offset, false); offset += 4;
|
||||
const bottom = view.getInt32(offset, false); offset += 4;
|
||||
const right = view.getInt32(offset, false); offset += 4;
|
||||
const channelCount = view.getUint16(offset, false); offset += 2;
|
||||
const channels = [];
|
||||
for (let c = 0; c < channelCount; c++) {
|
||||
const id = view.getInt16(offset, false); offset += 2;
|
||||
const length = psb ? Number(view.getBigUint64(offset, false)) : view.getUint32(offset, false);
|
||||
offset += longLen;
|
||||
channels.push({ id, length });
|
||||
}
|
||||
offset += 4; // '8BIM'
|
||||
const blendKey = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]); offset += 4;
|
||||
const opacity = bytes[offset++] / 255;
|
||||
offset += 1; // clipping
|
||||
const flags = bytes[offset++];
|
||||
offset += 1; // filler
|
||||
const extraLength = view.getUint32(offset, false); offset += 4;
|
||||
const extraEnd = offset + extraLength;
|
||||
const maskLength = view.getUint32(offset, false); offset += 4 + maskLength;
|
||||
const rangesLength = view.getUint32(offset, false); offset += 4 + rangesLength;
|
||||
const nameLength = bytes[offset++];
|
||||
let name = '';
|
||||
for (let i = 0; i < nameLength; i++) name += String.fromCharCode(bytes[offset + i]);
|
||||
offset = extraEnd;
|
||||
metas.push({ top, left, bottom, right, channelCount, channels, blendKey, opacity, visible: (flags & 2) === 0, name });
|
||||
}
|
||||
// Channel image data follows the records; the metas loop above already
|
||||
// left `offset` at the start of the channel data.
|
||||
const layers = [];
|
||||
for (const meta of metas) {
|
||||
const lw = meta.right - meta.left;
|
||||
const lh = meta.bottom - meta.top;
|
||||
const count = lw * lh;
|
||||
const planes = { 0: new Uint8Array(count), 1: new Uint8Array(count), 2: new Uint8Array(count), '-1': new Uint8Array(count) };
|
||||
for (let c = 0; c < meta.channelCount; c++) {
|
||||
const channel = meta.channels[c];
|
||||
const compression = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
if (compression === COMPRESSION_RAW) {
|
||||
const plane = bytes.subarray(offset, offset + count);
|
||||
if (planes[channel.id]) planes[channel.id].set(plane);
|
||||
offset += count;
|
||||
} else if (compression === COMPRESSION_RLE) {
|
||||
const rowLengths = [];
|
||||
for (let y = 0; y < lh; y++) {
|
||||
rowLengths.push(psb ? view.getUint32(offset, false) : view.getUint16(offset, false));
|
||||
offset += psb ? 4 : 2;
|
||||
}
|
||||
const plane = planes[channel.id] || new Uint8Array(count);
|
||||
for (let y = 0; y < lh; y++) {
|
||||
const decoded = unpackBits(bytes, offset, rowLengths[y]);
|
||||
plane.set(decoded.subarray(0, lw), y * lw);
|
||||
offset += rowLengths[y];
|
||||
}
|
||||
planes[channel.id] = plane;
|
||||
} else {
|
||||
throw new Error('Unsupported PSD layer compression');
|
||||
}
|
||||
}
|
||||
const data = new Uint8ClampedArray(count * 4);
|
||||
const alpha = planes['-1'];
|
||||
for (let i = 0; i < count; i++) {
|
||||
data[i * 4] = planes[0][i];
|
||||
data[i * 4 + 1] = planes[1][i];
|
||||
data[i * 4 + 2] = planes[2][i];
|
||||
data[i * 4 + 3] = alpha ? alpha[i] : 255;
|
||||
}
|
||||
layers.push({
|
||||
name: meta.name,
|
||||
x: meta.left,
|
||||
y: meta.top,
|
||||
width: lw,
|
||||
height: lh,
|
||||
opacity: meta.opacity,
|
||||
visible: meta.visible,
|
||||
blendKey: meta.blendKey,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return { width, height, layers: layers.reverse() };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user