Files
MineGeo/lib/fabric-images.ts

99 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash } from "node:crypto";
import sharp from "sharp";
import { faces, type DatasetManifest } from "../shared/protocol";
type ImageIO = {
download: (path: string) => Promise<Buffer>;
save: (path: string, bytes: Buffer) => Promise<void>;
progress: (percent: number) => Promise<void>;
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);
}
}
}