import { cropRgba, decodePng } from "@/lib/services/imager/png-decode"; import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder"; interface FurniAsset { x?: number; y?: number; source?: string; } interface FurniFrame { frame: { x: number; y: number; w: number; h: number }; rotated?: boolean; } interface FurniNitroJson { name?: string; assets?: Record; spritesheet?: { frames?: Record }; } /** * Extract the furni catalog icon — the `{name}_icon_a` sprite — from a Nitro * bundle and return it as a standalone PNG, or null if the bundle has no icon * asset / usable frame. Used for sources that embed furni icons inside the * `.nitro` instead of serving a separate `{classname}_icon.png`. */ export function extractFurniIconPng(nitro: Buffer): Buffer | null { let json: FurniNitroJson; let png: Buffer; try { const parsed = parseNitroBundle(nitro); json = parsed.json as FurniNitroJson; png = parsed.png; } catch { return null; } const name = json.name; if (!name || !json.assets) return null; // The icon asset is conventionally `${name}_icon_a`; fall back to any // `${name}_icon*` the bundle exposes. const iconKey = json.assets[`${name}_icon_a`] ? `${name}_icon_a` : Object.keys(json.assets).find((k) => k.startsWith(`${name}_icon`)); if (!iconKey) return null; const asset = json.assets[iconKey]; // An asset may alias another's pixels via `source`; the frame key is the // bundle name prefixed onto the asset name. Different converters pack the // key with or without a trailing `.png`, so try both. const pixelAsset = asset.source ?? iconKey; const frames = json.spritesheet?.frames ?? {}; const frame = frames[`${name}_${pixelAsset}`] ?? frames[`${name}_${pixelAsset}.png`]; if (!frame || frame.rotated) return null; const { x, y, w, h } = frame.frame; if (w <= 0 || h <= 0) return null; try { const sheet = decodePng(png); const px = cropRgba(sheet.rgba, sheet.width, x, y, w, h); return encodePng(w, h, px); } catch { return null; } }