This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
@@ -2,302 +2,315 @@ import { inflateSync } from "node:zlib";
|
||||
import * as jpeg from "jpeg-js";
|
||||
|
||||
export interface SpriteImage {
|
||||
id: number;
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer; // RGBA
|
||||
id: number;
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer; // RGBA
|
||||
}
|
||||
|
||||
// ── DefineBitsLossless2 Parser (with alpha) ──────────────────────
|
||||
|
||||
export function parseBitsLossless2(data: Buffer): {
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
} {
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
|
||||
if (format === 5) {
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
const a = decompressed[srcOff];
|
||||
let r = decompressed[srcOff + 1];
|
||||
let g = decompressed[srcOff + 2];
|
||||
let b = decompressed[srcOff + 3];
|
||||
srcOff += 4;
|
||||
if (format === 5) {
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
const a = decompressed[srcOff];
|
||||
let r = decompressed[srcOff + 1];
|
||||
let g = decompressed[srcOff + 2];
|
||||
let b = decompressed[srcOff + 3];
|
||||
srcOff += 4;
|
||||
|
||||
// Un-premultiply alpha
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
// Un-premultiply alpha
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = a;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const a = decompressed[palOff];
|
||||
let r = decompressed[palOff + 1];
|
||||
let g = decompressed[palOff + 2];
|
||||
let b = decompressed[palOff + 3];
|
||||
// Un-premultiply alpha for palette entries (same as format 5)
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
palette.push([r, g, b, a]);
|
||||
palOff += 4;
|
||||
}
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = a;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const a = decompressed[palOff];
|
||||
let r = decompressed[palOff + 1];
|
||||
let g = decompressed[palOff + 2];
|
||||
let b = decompressed[palOff + 3];
|
||||
// Un-premultiply alpha for palette entries (same as format 5)
|
||||
if (a > 0 && a < 255) {
|
||||
r = Math.min(255, Math.round((r * 255) / a));
|
||||
g = Math.min(255, Math.round((g * 255) / a));
|
||||
b = Math.min(255, Math.round((b * 255) / a));
|
||||
}
|
||||
palette.push([r, g, b, a]);
|
||||
palOff += 4;
|
||||
}
|
||||
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 4;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 0];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 4;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 0];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width, height, pixels };
|
||||
return { id, width, height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsLossless Parser (without alpha) ──────────────────────
|
||||
|
||||
export function parseBitsLossless(data: Buffer): {
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
pixels: Buffer;
|
||||
} {
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
let offset = 0;
|
||||
const id = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const format = data.readUInt8(offset);
|
||||
offset += 1;
|
||||
const width = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
const height = data.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
let colorTableSize = 0;
|
||||
if (format === 3) {
|
||||
colorTableSize = data.readUInt8(offset) + 1;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
const compressed = data.subarray(offset);
|
||||
let decompressed: Buffer;
|
||||
try {
|
||||
decompressed = inflateSync(compressed);
|
||||
} catch {
|
||||
return { id, width, height, pixels: Buffer.alloc(width * height * 4) };
|
||||
}
|
||||
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
const pixels = Buffer.alloc(width * height * 4);
|
||||
|
||||
if (format === 5) {
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
srcOff++; // skip padding byte
|
||||
const r = decompressed[srcOff++];
|
||||
const g = decompressed[srcOff++];
|
||||
const b = decompressed[srcOff++];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = 255;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const r = decompressed[palOff];
|
||||
const g = decompressed[palOff + 1];
|
||||
const b = decompressed[palOff + 2];
|
||||
palette.push([r, g, b, 255]);
|
||||
palOff += 3;
|
||||
}
|
||||
if (format === 5) {
|
||||
let srcOff = 0;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (srcOff + 3 >= decompressed.length) break;
|
||||
srcOff++; // skip padding byte
|
||||
const r = decompressed[srcOff++];
|
||||
const g = decompressed[srcOff++];
|
||||
const b = decompressed[srcOff++];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = r;
|
||||
pixels[dstOff + 1] = g;
|
||||
pixels[dstOff + 2] = b;
|
||||
pixels[dstOff + 3] = 255;
|
||||
}
|
||||
}
|
||||
} else if (format === 3) {
|
||||
const palette: number[][] = [];
|
||||
let palOff = 0;
|
||||
for (let i = 0; i < colorTableSize; i++) {
|
||||
const r = decompressed[palOff];
|
||||
const g = decompressed[palOff + 1];
|
||||
const b = decompressed[palOff + 2];
|
||||
palette.push([r, g, b, 255]);
|
||||
palOff += 3;
|
||||
}
|
||||
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 3;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 255];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
const rowPadded = Math.ceil(width / 4) * 4;
|
||||
let srcOff = colorTableSize * 3;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = decompressed[srcOff + x];
|
||||
const color = palette[idx] || [0, 0, 0, 255];
|
||||
const dstOff = (y * width + x) * 4;
|
||||
pixels[dstOff] = color[0];
|
||||
pixels[dstOff + 1] = color[1];
|
||||
pixels[dstOff + 2] = color[2];
|
||||
pixels[dstOff + 3] = color[3];
|
||||
}
|
||||
srcOff += rowPadded;
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width, height, pixels };
|
||||
return { id, width, height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsJpeg2 Parser (JPEG without alpha) ───────────────────
|
||||
|
||||
export function parseBitsJpeg2(
|
||||
data: Buffer,
|
||||
data: Buffer,
|
||||
): { id: number; width: number; height: number; pixels: Buffer } | null {
|
||||
if (data.length < 6) return null;
|
||||
if (data.length < 6) return null;
|
||||
|
||||
const id = data.readUInt16LE(0);
|
||||
let jpegData = data.subarray(2);
|
||||
const id = data.readUInt16LE(0);
|
||||
let jpegData = data.subarray(2);
|
||||
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
jpegData.length >= 4 &&
|
||||
jpegData[0] === 0xff &&
|
||||
jpegData[1] === 0xd9 &&
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
jpegData.length >= 4 &&
|
||||
jpegData[0] === 0xff &&
|
||||
jpegData[1] === 0xd9 &&
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
}
|
||||
|
||||
// ── DefineBitsJpeg3 Parser (JPEG with alpha channel) ──────────────
|
||||
|
||||
export function parseBitsJpeg3(
|
||||
data: Buffer,
|
||||
data: Buffer,
|
||||
): { id: number; width: number; height: number; pixels: Buffer } | null {
|
||||
if (data.length < 6) return null;
|
||||
if (data.length < 6) return null;
|
||||
|
||||
const id = data.readUInt16LE(0);
|
||||
const alphaDataOffset = data.readUInt32LE(2);
|
||||
let jpegData = data.subarray(6, 6 + alphaDataOffset);
|
||||
const id = data.readUInt16LE(0);
|
||||
const alphaDataOffset = data.readUInt32LE(2);
|
||||
let jpegData = data.subarray(6, 6 + alphaDataOffset);
|
||||
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
jpegData.length >= 4 &&
|
||||
jpegData[0] === 0xff &&
|
||||
jpegData[1] === 0xd9 &&
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
// Strip erroneous JPEG header (FF D9 FF D1) if present
|
||||
if (
|
||||
jpegData.length >= 4 &&
|
||||
jpegData[0] === 0xff &&
|
||||
jpegData[1] === 0xd9 &&
|
||||
jpegData[2] === 0xff &&
|
||||
jpegData[3] === 0xd1
|
||||
) {
|
||||
jpegData = jpegData.subarray(4);
|
||||
}
|
||||
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
const dims = parseJpegDimensions(jpegData);
|
||||
if (!dims) return null;
|
||||
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
const pixels = decodeJpegToRGBA(jpegData, dims.width, dims.height);
|
||||
|
||||
// Apply zlib-compressed alpha channel
|
||||
const compressedAlpha = data.subarray(6 + alphaDataOffset);
|
||||
if (compressedAlpha.length > 0) {
|
||||
try {
|
||||
const alpha = inflateSync(compressedAlpha);
|
||||
const pixelCount = dims.width * dims.height;
|
||||
for (let i = 0; i < pixelCount && i < alpha.length; i++) {
|
||||
pixels[i * 4 + 3] = alpha[i];
|
||||
}
|
||||
} catch {
|
||||
// Alpha decompression failed, keep fully opaque
|
||||
}
|
||||
}
|
||||
// Apply zlib-compressed alpha channel
|
||||
const compressedAlpha = data.subarray(6 + alphaDataOffset);
|
||||
if (compressedAlpha.length > 0) {
|
||||
try {
|
||||
const alpha = inflateSync(compressedAlpha);
|
||||
const pixelCount = dims.width * dims.height;
|
||||
for (let i = 0; i < pixelCount && i < alpha.length; i++) {
|
||||
pixels[i * 4 + 3] = alpha[i];
|
||||
}
|
||||
} catch {
|
||||
// Alpha decompression failed, keep fully opaque
|
||||
}
|
||||
}
|
||||
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
return { id, width: dims.width, height: dims.height, pixels };
|
||||
}
|
||||
|
||||
// ── JPEG Dimension Parser (reads SOF0/SOF2 markers) ───────────────
|
||||
|
||||
export function parseJpegDimensions(jpeg: Buffer): { width: number; height: number } | null {
|
||||
let offset = 0;
|
||||
while (offset < jpeg.length - 1) {
|
||||
if (jpeg[offset] !== 0xff) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
export function parseJpegDimensions(
|
||||
jpeg: Buffer,
|
||||
): { width: number; height: number } | null {
|
||||
let offset = 0;
|
||||
while (offset < jpeg.length - 1) {
|
||||
if (jpeg[offset] !== 0xff) {
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const marker = jpeg[offset + 1];
|
||||
offset += 2;
|
||||
const marker = jpeg[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
// SOF0 (Baseline), SOF1 (Extended), SOF2 (Progressive)
|
||||
if (marker >= 0xc0 && marker <= 0xc2) {
|
||||
if (offset + 7 > jpeg.length) return null;
|
||||
// Skip length (2 bytes) + precision (1 byte)
|
||||
const height = jpeg.readUInt16BE(offset + 3);
|
||||
const width = jpeg.readUInt16BE(offset + 5);
|
||||
return { width, height };
|
||||
}
|
||||
// SOF0 (Baseline), SOF1 (Extended), SOF2 (Progressive)
|
||||
if (marker >= 0xc0 && marker <= 0xc2) {
|
||||
if (offset + 7 > jpeg.length) return null;
|
||||
// Skip length (2 bytes) + precision (1 byte)
|
||||
const height = jpeg.readUInt16BE(offset + 3);
|
||||
const width = jpeg.readUInt16BE(offset + 5);
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// Skip non-SOF markers with length field
|
||||
if (marker === 0xd8 || marker === 0xd9) continue; // SOI / EOI
|
||||
if (marker >= 0xd0 && marker <= 0xd7) continue; // RST markers
|
||||
if (offset + 2 > jpeg.length) break;
|
||||
const segLen = jpeg.readUInt16BE(offset);
|
||||
offset += segLen;
|
||||
}
|
||||
return null;
|
||||
// Skip non-SOF markers with length field
|
||||
if (marker === 0xd8 || marker === 0xd9) continue; // SOI / EOI
|
||||
if (marker >= 0xd0 && marker <= 0xd7) continue; // RST markers
|
||||
if (offset + 2 > jpeg.length) break;
|
||||
const segLen = jpeg.readUInt16BE(offset);
|
||||
offset += segLen;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── JPEG to RGBA decoder using jpeg-js ──────────────────────────────
|
||||
|
||||
export function decodeJpegToRGBA(jpegData: Buffer, width: number, height: number): Buffer {
|
||||
try {
|
||||
const decoded = jpeg.decode(jpegData, { useTArray: true, formatAsRGBA: true });
|
||||
// jpeg-js returns RGBA data directly
|
||||
return Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength);
|
||||
} catch {
|
||||
// Fallback: transparent pixels if JPEG decode fails
|
||||
return Buffer.alloc(width * height * 4);
|
||||
}
|
||||
export function decodeJpegToRGBA(
|
||||
jpegData: Buffer,
|
||||
width: number,
|
||||
height: number,
|
||||
): Buffer {
|
||||
try {
|
||||
const decoded = jpeg.decode(jpegData, {
|
||||
useTArray: true,
|
||||
formatAsRGBA: true,
|
||||
});
|
||||
// jpeg-js returns RGBA data directly
|
||||
return Buffer.from(
|
||||
decoded.data.buffer,
|
||||
decoded.data.byteOffset,
|
||||
decoded.data.byteLength,
|
||||
);
|
||||
} catch {
|
||||
// Fallback: transparent pixels if JPEG decode fails
|
||||
return Buffer.alloc(width * height * 4);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user