import type { PathLike } from "fs"; import { access } from "fs/promises"; import { dirname, resolve } from "path"; import sharp, { type OverlayOptions } from "sharp"; import { parseArgs } from "util"; import { formatImage, makeKZ } from "./common"; import { readFile } from "fs/promises"; import { writeFile } from "fs/promises"; import { rm } from "fs/promises"; import { mkdir } from "fs/promises"; import { stat } from "fs/promises"; import { readdir } from "fs/promises"; const { values: args } = parseArgs({ args: Bun.argv, options: { images: { type: "string", short: "i", default: "images/images.json" }, imagedir: { type: "string", short: "d", default: "images" }, outdir: { type: "string", short: "o", default: "data" }, kzoutdir: { type: "string", short: "k", default: "data-kz" }, throw: { type: "boolean", short: "t", default: false } }, strict: true, allowPositionals: true }); const dirExists = async(path: PathLike) => access(path).then(() => true, () => false); const imagesPath = resolve(args.images ?? "images.json"); const imageDir = resolve(args.imagedir ?? dirname(imagesPath)); const outDir = resolve(args.outdir ?? "data"); const kzOutDir = resolve(args.kzoutdir ?? "data-kz"); const framePercent = 0.03125; const frameColors = [0xA47627, 0xA45226, 0x944421, 0xAC581D, 0x8C341C, 0xAC641D, 0xAB6C25, 0xA44424, 0xAC572C, 0xAC4C24, 0xA87824]; const throwOnMissing = !!args.throw; if (!await dirExists(imageDir)) { console.error("Image directory %s does not exist.", imageDir); process.exit(1); } const ap = (p: string) => resolve(p, "assets/minecraft/textures/painting"); // const ap = (p: string) => resolve(p, "../img"); await rm(`${outDir}/assets`, { recursive: true, force: true }); await rm(`${kzOutDir}/assets`, { recursive: true, force: true }); await mkdir(ap(outDir), { recursive: true }); await mkdir(ap(kzOutDir), { recursive: true }); interface Image { animation?: { frametime?: number; }; frames?: { start: number; end?: number; } | number[]; kz: [x: number, y: number] | null; name: string; resize?: [width: number, height: number]; size: string; } interface Images { images: Array; sizes: Record; } const images = await Bun.file(imagesPath).json() as Images; const [baseWidth, baseHeight] = images.sizes["1x1"]; let kz = await makeKZ(baseWidth); const kzCleanup: string[] = []; for (const image of images.images) { let img: string | string[]; if (await stat(`${imageDir}/${image.name}`).then(s => s.isDirectory(), () => false)) { img = await readdir(`${imageDir}/${image.name}`).then(files => files.map(f => `${imageDir}/${image.name}/${f}`).sort()); } else { const [file] = await Array.fromAsync(new Bun.Glob(`${image.name}.*`).scan({ onlyFiles: true, cwd: imageDir })); if (!file) { console.error("Image %s does not exist.", image.name); if (throwOnMissing) { process.exit(1); } continue; } img = `${imageDir}/${file}` } console.log("Processing %s", image.name); const [resizeWidth, resizeHeight] = image.resize ?? []; const { isImage, result, kzFile } = await formatImage(image.name, img, frameColors, framePercent, ...images.sizes[image.size], resizeWidth, resizeHeight, image.frames, image.kz !== null); await result.toFile(`${ap(outDir)}/${image.name}.png`); if (!isImage) { await writeFile(`${ap(outDir)}/${image.name}.png.mcmeta`, JSON.stringify({ animation: image.animation ?? {} })); } if (image.kz && kzFile !== null) { kz = await sharp(kz) .composite([{ input: await sharp(kzFile === true ? `${ap(outDir)}/${image.name}.png` : kzFile).resize(images.sizes[image.size][0], images.sizes[image.size][1]).toBuffer(), top: baseHeight * image.kz[1], left: baseWidth * image.kz[0] }]) .toBuffer(); if (kzFile !== true) { kzCleanup.push(kzFile); } } } await sharp(kz).toFile(`${ap(kzOutDir)}/paintings_kristoffer_zetterstrand.png`); for (const file of kzCleanup) { await rm(file); }