UNPKG

unreal.js

Version:

A pak reader for games like VALORANT & Fortnite written in Node.JS

215 lines (214 loc) 9.11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PixelFormatInfo = exports.Image = void 0; const optionalRequire = require("optional-require")(require); const canvas = optionalRequire("canvas") || null; const dxt = optionalRequire("dxt-js") || null; const BC5_1 = require("./BC5"); class Image { static convert(tex, texture, mip, config) { checkResources(); if (!texture) { texture = tex.getFirstTexture(); return this.convert(tex, texture, mip || texture.getFirstLoadedMip()); } const data = mip.data.data; const width = mip.sizeX; const height = mip.sizeY; const formatElement = PixelFormatInfo[texture.pixelFormat]; if (!formatElement) throw new Error(`Unknown pixel format: ${texture.pixelFormat}`); const pixelSize = formatElement.float ? 16 : 4; const size = width * height * pixelSize; const dst = Buffer.alloc(size); const format = formatElement.pixelFormat; // i hate javascript switch/case, it sucks, looks better than if/else tho switch (format) { case "PF_RGB8": let d = 0; let s = 0; for (let i = 0; i < width * height; ++i) { // BGRA -> RGBA dst[d] = data[s + 2]; dst[d + 1] = data[s + 1]; dst[d + 2] = data[s]; dst[d + 3] = 255; d += 4; s += 3; } break; case "PF_R8G8B8A8": case "PF_RGBA8": data.copy(dst, 0, 0, width * height * 4); break; case "PF_BGRA8": case "PF_B8G8R8A8": let d0 = 0; let s0 = 0; for (let i0 = 0; i0 < width * height; ++i0) { // BGRA -> RGBA dst[d0] = data[s0 + 2]; dst[d0 + 1] = data[s0 + 1]; dst[d0 + 2] = data[s0]; dst[d0 + 3] = data[s0 + 3]; d0 += 4; s0 += 4; } break; case "PF_RGBA4": let d1 = 0; let s1 = 0; for (let i1 = 0; i1 < width * height; ++i1) { const b1 = data[s1]; const b2 = data[s1 + 1]; // BGRA -> RGBA dst[d1] = b2 & 0xF0; dst[d1 + 1] = (b2 & 0xF) << 4; dst[d1 + 2] = b1 & 0xF0; dst[d1 + 3] = (b1 & 0xF) << 4; d1 += 4; s1 += 2; } break; case "PF_G8": let d2 = 0; let s2 = 0; for (let i2 = 0; i2 < width * height; ++i2) { const b = data[s2]; dst[d2] = b; dst[d2 + 1] = b; dst[d2 + 2] = b; dst[d2 + 3] = 255; d2 += 1; s2 += 4; } break; case "PF_V8U8": case "PF_V8U8_2": let d3 = 0; let s3 = 0; const offset = format === "PF_V8U8" ? 128 : 0; for (let i3 = 0; i3 < width * height; ++i3) { const u = data[s3] + offset; const v = data[s3 + 1] + offset; data[d3] = u; data[d3 + 1] = v; const uf = (u - offset) / 255.0 * 2 - 1; const vf = (v - offset) / 255.0 * 2 - 1; const t = 1.0 - uf * uf - vf * vf; if (t >= 0) data[d3 + 2] = 255 - 255 * Math.floor(Math.sqrt(t)); else data[d3 + 2] = 255; data[d3 + 3] = 255; s3 += 2; d3 += 4; } break; // TODO these are android, maybe ill do it later case PF_ASTC_4x4, PF_ASTC_6x6, PF_ASTC_8x8, PF_ASTC_10x10, PF_ASTC_12x12 // All DXT formats case "PF_DXT1": case "PF_DXT3": case "PF_DXT5": case "PF_DXT5N": let form; if (format === "PF_DXT5" || format === "PF_DXT5N") form = dxt.flags.DXT5; else if (format === "PF_DXT3") form = dxt.flags.DXT3; else form = dxt.flags.DXT1; const dxtRes = dxt.decompress(data, width, height, form); const decompressed = Buffer.from(dxtRes); decompressed.copy(dst, 0, 0, width * height * 4); break; case "PF_BC5": const rgb = BC5_1.readBC5(data, width, height); return rgbBufferToImage(rgb, width, height, config); } return rgbaBufferToImage(dst, width, height, config); } } exports.Image = Image; class PixelFormatInfo { constructor(blockSizeX, blockSizeY, bytesPerBlock, x360AlignX, x360AlignY, float, pixelFormat) { this.blockSizeX = blockSizeX; this.blockSizeY = blockSizeY; this.bytesPerBlock = bytesPerBlock; this.x360AlignX = x360AlignX; this.x360AlignY = x360AlignY; this.float = float; this.pixelFormat = pixelFormat; } } exports.PixelFormatInfo = PixelFormatInfo; PixelFormatInfo.PF_G8 = new PixelFormatInfo(1, 1, 1, 64, 64, false, "PF_G8"); PixelFormatInfo.PF_RGB8 = new PixelFormatInfo(1, 1, 3, 0, 0, false, "PF_RGB8"); PixelFormatInfo.PF_RGBA8 = new PixelFormatInfo(1, 1, 4, 32, 32, false, "PF_RGBA8"); PixelFormatInfo.PF_R8G8B8A8 = new PixelFormatInfo(1, 1, 4, 32, 32, false, "PF_R8G8B8A8"); PixelFormatInfo.PF_BGRA8 = new PixelFormatInfo(1, 1, 4, 32, 32, false, "PF_BGRA8"); PixelFormatInfo.PF_B8G8R8A8 = new PixelFormatInfo(1, 1, 4, 32, 32, false, "PF_B8G8R8A8"); PixelFormatInfo.PF_DXT1 = new PixelFormatInfo(4, 4, 8, 128, 128, false, "PF_DXT1"); PixelFormatInfo.PF_DXT3 = new PixelFormatInfo(4, 4, 16, 128, 128, false, "PF_DXT3"); PixelFormatInfo.PF_DXT5 = new PixelFormatInfo(4, 4, 16, 128, 128, false, "PF_DXT5"); PixelFormatInfo.PF_DXT5N = new PixelFormatInfo(4, 4, 16, 128, 128, false, "PF_DXT5N"); PixelFormatInfo.PF_V8U8 = new PixelFormatInfo(1, 1, 2, 64, 32, false, "PF_V8U8"); PixelFormatInfo.PF_V8U8_2 = new PixelFormatInfo(1, 1, 2, 64, 32, false, "PF_V8U8_2"); PixelFormatInfo.PF_BC5 = new PixelFormatInfo(4, 4, 16, 0, 0, false, "PF_BC5"); PixelFormatInfo.PF_RGBA4 = new PixelFormatInfo(1, 1, 2, 0, 0, false, "PF_RGBA4"); function checkResources() { let err = ""; const missingCanvas = canvas == null; const missingDxt = dxt == null; if (missingCanvas) err += "Module 'canvas' is required for image conversion. Please install it using 'npm i canvas'!"; if (missingDxt) err += ((missingCanvas ? "\n" : "") + "Module 'dxt-js' is required for image conversion. Please install it using 'npm i dxt-js'!"); if (err !== "") throw new Error(err); } function rgbaBufferToImage(rgba, width, height, config) { checkResources(); const img = canvas.createCanvas(width, height); const ctx = img.getContext("2d"); applyConfig(ctx, config); const imageData = ctx.createImageData(width, height); const len = imageData.data.length; let t = 0; // TODO might impact performance for (let i = 0; i < len; i += 4) { imageData.data[i] = rgba[t]; imageData.data[i + 1] = rgba[t + 1]; imageData.data[i + 2] = rgba[t + 2]; imageData.data[i + 3] = rgba[t + 3]; t += 4; } ctx.putImageData(imageData, 0, 0); ctx.translate(.5, .5); return img.toBuffer("image/png", { compressionLevel: 3 }); } function rgbBufferToImage(rgb, width, height, config) { checkResources(); const img = canvas.createCanvas(width, height); const ctx = img.getContext("2d"); applyConfig(ctx, config); const imageData = ctx.createImageData(width, height); const len = imageData.data.length; let t = 0; // TODO might impact performance for (let i = 0; i < len; i += 4) { imageData.data[i] = rgb[t]; imageData.data[i + 1] = rgb[t + 1]; imageData.data[i + 2] = rgb[t + 2]; imageData.data[i + 3] = 255; t += 3; } ctx.putImageData(imageData, 0, 0); ctx.translate(.5, 5); return img.toBuffer("image/png", { compressionLevel: 3 }); } function applyConfig(ctx, config) { ctx.imageSmoothingEnabled = config?.imageSmoothingEnabled || false; ctx.imageSmoothingQuality = config?.imageSmoothingQuality || "medium"; ctx.quality = config?.quality || "good"; }