UNPKG

pw-guild-icon-parser

Version:

Parser for Perfect World guild icon lists - converts PNG icons to DDS atlas format with DXT5 compression

244 lines 8.29 kB
/** * DXT5 compression with alpha channel support * Compresses 4x4 pixel blocks to 16 bytes (8 bytes for alpha, 8 bytes for color) */ /** * Compress a 4x4 RGBA block to DXT5 format */ export function compressDXT5Block(rgbaData, offset, width) { // Extract 4x4 block const block = []; for (let y = 0; y < 4; y++) { for (let x = 0; x < 4; x++) { const idx = offset + (y * width + x) * 4; if (idx + 3 < rgbaData.length) { block.push(rgbaData[idx]); // R block.push(rgbaData[idx + 1]); // G block.push(rgbaData[idx + 2]); // B block.push(rgbaData[idx + 3]); // A } else { // Pad with transparent black block.push(0, 0, 0, 0); } } } // Compress alpha channel (8 bytes) const alphaBlock = compressAlphaBlock(block); // Compress color (RGB, ignoring alpha) - DXT1 style (8 bytes) const colorBlock = compressColorBlock(block); return Buffer.concat([alphaBlock, colorBlock]); } /** * Compress alpha channel for 4x4 block * Returns 8 bytes: 2 alpha endpoints + 6-bit indices */ function compressAlphaBlock(block) { // Extract alpha values (every 4th value starting at index 3) const alphas = []; for (let i = 3; i < block.length; i += 4) { alphas.push(block[i]); } // Find min and max alpha let minAlpha = 255; let maxAlpha = 0; for (const alpha of alphas) { if (alpha < minAlpha) minAlpha = alpha; if (alpha > maxAlpha) maxAlpha = alpha; } // If min == max, we need to ensure they're different for the algorithm if (minAlpha === maxAlpha) { if (maxAlpha > 0) { minAlpha = Math.max(0, maxAlpha - 1); } else { maxAlpha = Math.min(255, minAlpha + 1); } } const result = Buffer.alloc(8); result.writeUInt8(minAlpha, 0); result.writeUInt8(maxAlpha, 1); // Calculate indices for each pixel const indices = []; for (let i = 0; i < 16; i++) { const alpha = alphas[i]; let index; if (maxAlpha > minAlpha) { // Calculate 6 interpolation values const a0 = minAlpha; const a1 = maxAlpha; const a2 = Math.floor((6 * a0 + 1 * a1) / 7); const a3 = Math.floor((5 * a0 + 2 * a1) / 7); const a4 = Math.floor((4 * a0 + 3 * a1) / 7); const a5 = Math.floor((3 * a0 + 4 * a1) / 7); const a6 = Math.floor((2 * a0 + 5 * a1) / 7); const a7 = Math.floor((1 * a0 + 6 * a1) / 7); // Find closest value const values = [a0, a1, a2, a3, a4, a5, a6, a7]; let minDist = Infinity; index = 0; for (let j = 0; j < 8; j++) { const dist = Math.abs(alpha - values[j]); if (dist < minDist) { minDist = dist; index = j; } } } else { index = 0; } indices.push(index); } // Pack 3-bit indices into 6 bytes (48 bits total for 16 pixels) // DXT5 alpha block format: bytes 2-7 contain 16 3-bit indices // Packed as: [pixel0(3 bits)][pixel1(3 bits)][pixel2(3 bits)]... // This spans across byte boundaries // Use BigInt to handle large bit shifts safely let bitBuffer = BigInt(0); let bitCount = 0; let byteOffset = 2; for (let i = 0; i < 16; i++) { // Ensure index is only 3 bits const index = BigInt(indices[i] & 0x7); // Add to bit buffer bitBuffer |= (index << BigInt(bitCount)); bitCount += 3; // Write bytes as they fill up while (bitCount >= 8 && byteOffset < 8) { result.writeUInt8(Number(bitBuffer & BigInt(0xFF)), byteOffset); byteOffset++; // Shift right by 8 bits bitBuffer = bitBuffer >> BigInt(8); bitCount -= 8; } } // Write remaining bits if any if (bitCount > 0 && byteOffset < 8) { result.writeUInt8(Number(bitBuffer & BigInt(0xFF)), byteOffset); } return result; } /** * Compress RGB color for 4x4 block (DXT1 style) * Returns 8 bytes: 2 color endpoints + 2-bit indices */ function compressColorBlock(block) { // Extract RGB values const colors = []; for (let i = 0; i < block.length; i += 4) { colors.push([block[i], block[i + 1], block[i + 2]]); // R, G, B } // Find two representative colors (min and max in color space) // Use simple method: find colors that minimize error let bestColor0 = colors[0]; let bestColor1 = colors[1]; let minError = Infinity; // Try different color pairs for (let i = 0; i < colors.length; i++) { for (let j = i + 1; j < colors.length; j++) { const error = calculateColorError(colors, colors[i], colors[j]); if (error < minError) { minError = error; bestColor0 = colors[i]; bestColor1 = colors[j]; } } } // Convert RGB565 const color0 = rgbTo565(bestColor0[0], bestColor0[1], bestColor0[2]); const color1 = rgbTo565(bestColor1[0], bestColor1[1], bestColor1[2]); const result = Buffer.alloc(8); result.writeUInt16LE(color0, 0); result.writeUInt16LE(color1, 2); // Calculate indices (using BigInt to avoid overflow) let indices = BigInt(0); for (let i = 0; i < 16; i++) { const color = colors[i]; const index = findClosestColorIndex(color, bestColor0, bestColor1, color0, color1); indices |= (BigInt(index) << BigInt(i * 2)); } // Convert to uint32, ensuring it fits const indicesUint32 = Number(indices & BigInt(0xFFFFFFFF)); result.writeUInt32LE(indicesUint32, 4); return result; } function rgbTo565(r, g, b) { return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); } function findClosestColorIndex(color, c0, c1, color0, color1) { // Calculate 4 interpolated colors const colors = [ c0, c1, [ Math.floor((2 * c0[0] + c1[0]) / 3), Math.floor((2 * c0[1] + c1[1]) / 3), Math.floor((2 * c0[2] + c1[2]) / 3) ], [ Math.floor((c0[0] + 2 * c1[0]) / 3), Math.floor((c0[1] + 2 * c1[1]) / 3), Math.floor((c0[2] + 2 * c1[2]) / 3) ] ]; let minDist = Infinity; let bestIndex = 0; for (let i = 0; i < 4; i++) { const dist = colorDistance(color, colors[i]); if (dist < minDist) { minDist = dist; bestIndex = i; } } return bestIndex; } function colorDistance(c1, c2) { const dr = c1[0] - c2[0]; const dg = c1[1] - c2[1]; const db = c1[2] - c2[2]; return dr * dr + dg * dg + db * db; } function calculateColorError(colors, c0, c1) { let totalError = 0; for (const color of colors) { const index = findClosestColorIndex(color, c0, c1, 0, 0); const colorsArr = [ c0, c1, [ Math.floor((2 * c0[0] + c1[0]) / 3), Math.floor((2 * c0[1] + c1[1]) / 3), Math.floor((2 * c0[2] + c1[2]) / 3) ], [ Math.floor((c0[0] + 2 * c1[0]) / 3), Math.floor((c0[1] + 2 * c1[1]) / 3), Math.floor((c0[2] + 2 * c1[2]) / 3) ] ]; totalError += colorDistance(color, colorsArr[index]); } return totalError; } /** * Compress entire image to DXT5 format */ export function compressDXT5(width, height, rgbaData) { const blocks = []; const blockWidth = Math.ceil(width / 4); const blockHeight = Math.ceil(height / 4); for (let blockY = 0; blockY < blockHeight; blockY++) { for (let blockX = 0; blockX < blockWidth; blockX++) { const x = blockX * 4; const y = blockY * 4; const offset = (y * width + x) * 4; const block = compressDXT5Block(rgbaData, offset, width); blocks.push(block); } } return Buffer.concat(blocks); } //# sourceMappingURL=dxt5.js.map