import { createHash } from "node:crypto"; import sharp from "sharp"; import { faces, type DatasetManifest } from "../shared/protocol"; type ImageIO = { download: (path: string) => Promise; save: (path: string, bytes: Buffer) => Promise; progress: (percent: number) => Promise; signal: AbortSignal; mapOnly?: boolean; }; // Process one image at a time. Vercel needs neither a local capture directory // nor a Minecraft/Chromium process for images already captured by the mod. export async function processFabricImages( manifest: DatasetManifest, io: ImageIO, ) { if (manifest.source !== "fabric" && !manifest.mediaFormat) throw new Error("A Fabric capture is required"); const format = manifest.mediaFormat ?? "png"; const assets = new Map(manifest.assets.map((asset) => [asset.path, asset])); const points = io.mapOnly ? [] : (manifest.availableViewpoints ?? manifest.viewpoints); const total = 1 + points.length * faces.length; let completed = 0; async function read(path: string) { io.signal.throwIfAborted(); const asset = assets.get(path); if (!asset) throw new Error(`Missing export asset: ${path}`); const bytes = await io.download(path); io.signal.throwIfAborted(); if ( bytes.length !== asset.bytes || createHash("sha256").update(bytes).digest("hex") !== asset.sha256 ) throw new Error(`Asset checksum/size mismatch: ${path}`); return bytes; } async function save(path: string, bytes: Buffer) { io.signal.throwIfAborted(); await io.save(path, bytes); io.signal.throwIfAborted(); } const map = await read(`map.${format}`); const mapMeta = await sharp(map, { limitInputPixels: 16777216 }).metadata(); if ( mapMeta.format !== format || !mapMeta.width || !mapMeta.height || (mapMeta.pages ?? 1) !== 1 ) throw new Error("The captured map has an invalid image format"); await save( "map.webp", await sharp(map, { limitInputPixels: 16777216 }) .webp({ lossless: true }) .toBuffer(), ); await io.progress((95 * ++completed) / total); for (const point of points) { for (const face of faces) { const bytes = await read(`${point.id}/${face}.${format}`); const meta = await sharp(bytes, { limitInputPixels: 16777216, }).metadata(); if ( meta.format !== format || (meta.pages ?? 1) !== 1 || !meta.width || meta.width !== meta.height || meta.width < 256 || meta.width > 2048 ) throw new Error( format === "png" ? "Panorama faces must be square PNGs, 256–2048 pixels" : "Panorama faces must be square WebPs, 256–2048 pixels", ); await save( `${point.id}/${face}.webp`, await sharp(bytes, { limitInputPixels: 16777216 }) .webp({ quality: 85 }) .toBuffer(), ); if (face === "nz") await save( `${point.id}/preview.webp`, await sharp(bytes, { limitInputPixels: 16777216 }) .resize(512, 512) .webp({ quality: 76 }) .toBuffer(), ); await io.progress((95 * ++completed) / total); } } }