// 기술 역량 원본 이미지를 웹 전송용 WebP로 변환합니다. // 실행: node scripts/generate-capability-webp.mjs [파일명 일부] [--force] import fs from "node:fs"; import path from "node:path"; import vm from "node:vm"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const force = process.argv.includes("--force"); const filters = process.argv.slice(2).filter((argument) => !argument.startsWith("--")); const context = { window: {} }; vm.runInNewContext(fs.readFileSync(path.join(root, "js", "capabilities-data.js"), "utf8"), context); const sources = new Map(); for (const item of context.window.CAPABILITY_PROJECTS) { if (item.image) sources.set(item.image, "background"); if (item.objectImage) sources.set(item.objectImage, "object"); if (item.objectImageSecondary) sources.set(item.objectImageSecondary, "object"); } function dimensions(source) { const output = execFileSync("sips", ["-g", "pixelWidth", "-g", "pixelHeight", source], { encoding: "utf8", }); return { width: Number(output.match(/pixelWidth: (\d+)/)?.[1]), height: Number(output.match(/pixelHeight: (\d+)/)?.[1]), }; } function optimizedPath(source) { return source .replace("assets/images/", "assets/images-webp/") .replace(/\.(?:png|jpe?g)$/i, ".webp"); } const assets = Array.from(sources, ([relativeSource, type]) => { const source = path.join(root, relativeSource); const target = path.join(root, optimizedPath(relativeSource)); const sourceStat = fs.statSync(source); const targetStat = fs.existsSync(target) ? fs.statSync(target) : null; return { relativeSource, type, source, target, sourceStat, targetStat }; }).filter(({ relativeSource }) => !filters.length || filters.some((filter) => relativeSource.includes(filter))); if (!assets.length) { throw new Error(`일치하는 기술 역량 이미지가 없습니다: ${filters.join(", ")}`); } // 삭제된 결과물을 먼저 복구하고, 나머지는 데이터에 정의된 순서대로 처리합니다. assets.sort((a, b) => Number(Boolean(a.targetStat)) - Number(Boolean(b.targetStat))); const pending = assets.filter(({ sourceStat, targetStat }) => force || !targetStat || targetStat.mtimeMs < sourceStat.mtimeMs); let sourceBytes = assets.reduce((total, { sourceStat }) => total + sourceStat.size, 0); let outputBytes = assets.reduce((total, asset) => pending.includes(asset) ? total : total + asset.targetStat.size, 0); for (const [index, { relativeSource, type, source, target }] of pending.entries()) { fs.mkdirSync(path.dirname(target), { recursive: true }); console.log(`[${index + 1}/${pending.length}] ${relativeSource}`); const { width, height } = dimensions(source); const maxWidth = type === "background" ? 2560 : 2400; const maxHeight = type === "background" ? 1800 : 2400; const scale = Math.min(1, maxWidth / width, maxHeight / height); const args = ["-quiet", "-mt", "-m", "6", "-q", type === "background" ? "80" : "86"]; if (type === "background") args.push("-sharp_yuv"); else args.push("-alpha_q", "100", "-exact"); if (scale < 1) args.push("-resize", String(Math.round(width * scale)), String(Math.round(height * scale))); args.push(source, "-o", target); execFileSync("cwebp", args, { stdio: "inherit" }); outputBytes += fs.statSync(target).size; } const savedPercent = Math.round((1 - outputBytes / sourceBytes) * 100); console.log( `${pending.length ? `Generated ${pending.length}` : "Checked"} WebP assets: ` + `${(sourceBytes / 1048576).toFixed(1)}MB -> ${(outputBytes / 1048576).toFixed(1)}MB (${savedPercent}% reduction)`, );