LCE-Emerald-Launcher/src/services/SwfService.ts

278 lines
9.7 KiB
TypeScript

import pako from "pako";
export interface SwfTag {
code: number;
data: Uint8Array;
}
export interface SwfImage {
id: number;
type: "jpeg" | "png" | "lossless" | "unknown" | "gif";
name?: string;
data: Uint8Array;
alphaData?: Uint8Array;
width?: number;
height?: number;
format?: number;
hasAlpha?: boolean;
}
export class SwfService {
static parse(buffer: Uint8Array): { version: number; compressed: boolean; frameHeader: Uint8Array; tags: SwfTag[] } {
if (buffer.length < 8) throw new Error("Invalid SWF: file too small");
const sig = String.fromCharCode(buffer[0], buffer[1], buffer[2]);
const version = buffer[3];
let data: Uint8Array;
if (sig === "FWS") {
data = buffer.slice(8);
} else if (sig === "CWS") {
try {
data = pako.inflate(buffer.slice(8));
} catch (e) {
throw new Error("Failed to decompress CWS SWF");
}
} else {
throw new Error(`Unsupported SWF signature: ${sig}`);
}
let offset = 0;
const nbits = data[0] >> 3;
const rectBytes = Math.ceil((5 + nbits * 4) / 8);
const frameHeaderLength = rectBytes + 4;
const frameHeader = data.slice(0, frameHeaderLength);
offset = frameHeaderLength;
const tags: SwfTag[] = [];
while (offset < data.length) {
const tagHeader = data[offset] | (data[offset + 1] << 8);
offset += 2;
const code = tagHeader >> 6;
let length = tagHeader & 0x3f;
if (length === 0x3f) {
length = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
offset += 4;
}
const tagData = data.slice(offset, offset + length);
tags.push({ code, data: tagData });
offset += length;
if (code === 0) break;
}
return { version, compressed: sig === "CWS", frameHeader, tags };
}
static serialize(version: number, compressed: boolean, frameHeader: Uint8Array, tags: SwfTag[]): Uint8Array {
const payloads: Uint8Array[] = [frameHeader];
let bodyLength = frameHeader.length;
for (const tag of tags) {
const isLong = tag.data.length >= 0x3f;
const headerLen = isLong ? 6 : 2;
const header = new Uint8Array(headerLen);
const h = (tag.code << 6) | (isLong ? 0x3f : tag.data.length);
header[0] = h & 0xff;
header[1] = (h >> 8) & 0xff;
if (isLong) {
header[2] = tag.data.length & 0xff;
header[3] = (tag.data.length >> 8) & 0xff;
header[4] = (tag.data.length >> 16) & 0xff;
header[5] = (tag.data.length >> 24) & 0xff;
}
payloads.push(header, tag.data);
bodyLength += headerLen + tag.data.length;
}
const uncompressedBody = new Uint8Array(bodyLength);
let offset = 0;
for (const p of payloads) {
uncompressedBody.set(p, offset);
offset += p.length;
}
const fileLength = 8 + uncompressedBody.length;
const finalBody = compressed ? pako.deflate(uncompressedBody) : uncompressedBody;
const result = new Uint8Array(8 + finalBody.length);
result.set(new TextEncoder().encode(compressed ? "CWS" : "FWS"), 0);
result[3] = version;
result[4] = fileLength & 0xff;
result[5] = (fileLength >> 8) & 0xff;
result[6] = (fileLength >> 16) & 0xff;
result[7] = (fileLength >> 24) & 0xff;
result.set(finalBody, 8);
return result;
}
static extractImages(tags: SwfTag[]): SwfImage[] {
const images: SwfImage[] = [];
const nameMap: Record<number, string> = {};
let jpegTables: Uint8Array | null = null;
for (const tag of tags) {
if (tag.code === 8) {
jpegTables = tag.data;
} else if (tag.code === 56 || tag.code === 76) {
const numAssets = tag.data[0] | (tag.data[1] << 8);
let ptr = 2;
for (let i = 0; i < numAssets; i++) {
const charId = tag.data[ptr] | (tag.data[ptr + 1] << 8);
ptr += 2;
let end = ptr;
while (end < tag.data.length && tag.data[end] !== 0) end++;
nameMap[charId] = new TextDecoder().decode(tag.data.slice(ptr, end));
ptr = end + 1;
}
} else if (tag.code === 6 || tag.code === 21) {
const charId = tag.data[0] | (tag.data[1] << 8);
const imgData = tag.data.slice(2);
const fixed = this.fixJpeg(imgData, tag.code === 6 ? jpegTables : null);
images.push({ id: charId, type: "jpeg", data: fixed, name: nameMap[charId] });
} else if (tag.code === 35) {
const charId = tag.data[0] | (tag.data[1] << 8);
const alphaOffset = tag.data[2] | (tag.data[3] << 8) | (tag.data[4] << 16) | (tag.data[5] << 24);
const imgData = tag.data.slice(6, 6 + alphaOffset);
const alphaRaw = tag.data.slice(6 + alphaOffset);
let alpha = undefined;
try { alpha = pako.inflate(alphaRaw); } catch (e) { }
images.push({ id: charId, type: "jpeg", data: this.fixJpeg(imgData), alphaData: alpha, name: nameMap[charId] });
} else if (tag.code === 20 || tag.code === 36) {
const charId = tag.data[0] | (tag.data[1] << 8);
const format = tag.data[2];
const width = tag.data[3] | (tag.data[4] << 8);
const height = tag.data[5] | (tag.data[6] << 8);
const hasAlpha = tag.code === 36;
const raw = tag.data.slice(tag.code === 20 ? 7 : 7);
images.push({
id: charId,
type: "lossless",
data: raw,
width,
height,
format,
hasAlpha,
name: nameMap[charId]
});
}
}
return images;
}
private static fixJpeg(data: Uint8Array, jtt: Uint8Array | null = null): Uint8Array {
let combined = data;
if (jtt && jtt.length > 0) {
let tLen = jtt.length;
if (jtt[tLen - 2] === 0xff && jtt[tLen - 1] === 0xd9) tLen -= 2;
let start = 0;
if (data[0] === 0xff && data[1] === 0xd8) start = 2;
combined = new Uint8Array(tLen + data.length - start);
combined.set(jtt.slice(0, tLen), 0);
combined.set(data.slice(start), tLen);
}
return combined;
}
static async decodeLosslessToRGBA(img: SwfImage): Promise<Uint8Array> {
if (img.type !== "lossless" || !img.width || !img.height) return new Uint8Array(0);
const decoded = pako.inflate(img.data);
const rgba = new Uint8Array(img.width * img.height * 4);
if (img.format === 3) {
const colorTableSize = decoded[0] + 1;
let ptr = 1;
const palette: number[][] = [];
for (let i = 0; i < colorTableSize; i++) {
if (img.hasAlpha) {
const a = decoded[ptr++];
const r_pm = decoded[ptr++];
const g_pm = decoded[ptr++];
const b_pm = decoded[ptr++];
palette.push([
a > 0 ? Math.min(255, (r_pm * 255) / a) : 0,
a > 0 ? Math.min(255, (g_pm * 255) / a) : 0,
a > 0 ? Math.min(255, (b_pm * 255) / a) : 0,
a
]);
} else {
palette.push([decoded[ptr++], decoded[ptr++], decoded[ptr++], 255]);
}
}
const rowStride = Math.ceil(img.width / 4) * 4;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const idx = decoded[ptr + y * rowStride + x];
const color = palette[idx] || [0, 0, 0, 0];
const offset = (y * img.width + x) * 4;
rgba[offset] = color[0];
rgba[offset + 1] = color[1];
rgba[offset + 2] = color[2];
rgba[offset + 3] = color[3];
}
}
} else if (img.format === 4) {
const rowStride = Math.ceil((img.width * 2) / 4) * 4;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const p = decoded[y * rowStride + x * 2] | (decoded[y * rowStride + x * 2 + 1] << 8);
const r = ((p >> 10) & 0x1f) << 3;
const g = ((p >> 5) & 0x1f) << 3;
const b = (p & 0x1f) << 3;
const offset = (y * img.width + x) * 4;
rgba[offset] = r;
rgba[offset + 1] = g;
rgba[offset + 2] = b;
rgba[offset + 3] = 255;
}
}
} else if (img.format === 5) {
let ptr = 0;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const a = decoded[ptr++];
const r_pm = decoded[ptr++];
const g_pm = decoded[ptr++];
const b_pm = decoded[ptr++];
const offset = (y * img.width + x) * 4;
if (img.hasAlpha) {
rgba[offset] = a > 0 ? Math.min(255, (r_pm * 255) / a) : 0;
rgba[offset + 1] = a > 0 ? Math.min(255, (g_pm * 255) / a) : 0;
rgba[offset + 2] = a > 0 ? Math.min(255, (b_pm * 255) / a) : 0;
rgba[offset + 3] = a;
} else {
rgba[offset] = r_pm;
rgba[offset + 1] = g_pm;
rgba[offset + 2] = b_pm;
rgba[offset + 3] = 255;
}
}
}
}
return rgba;
}
static updateImageTag(tags: SwfTag[], charId: number, newData: Uint8Array, type: SwfImage["type"]): SwfTag[] {
return tags.map(tag => {
if (tag.code === 6 || tag.code === 21 || tag.code === 35 || tag.code === 20 || tag.code === 36) {
const id = tag.data[0] | (tag.data[1] << 8);
if (id === charId) {
let payload: Uint8Array;
if (type === "jpeg") {
const header = new Uint8Array(tag.code === 35 ? 6 : 2);
header[0] = id & 0xff;
header[1] = (id >> 8) & 0xff;
if (tag.code === 35) {
payload = newData;
} else {
payload = new Uint8Array(2 + newData.length);
payload.set(header, 0);
payload.set(newData, 2);
}
} else {
payload = newData;
}
return { ...tag, data: payload };
}
}
return tag;
});
}
}