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 } }