import { parse, type RootNode, type ElementNode } from "svg-parser"; import fs from "fs"; import colors from "colors"; import * as path from "path"; import { Command } from "commander"; const program = new Command(); import { formatISO } from "date-fns"; import { fileTypeFromFile } from "file-type"; import klaw from "klaw"; import sharp from "sharp"; import cliProgress from "cli-progress"; import os, { loadavg } from "os"; import async from "async"; const packageJSON = JSON.parse( fs.readFileSync("package.json", { encoding: "utf-8" }) ); program .name(packageJSON.name) .description(packageJSON.description) .version(packageJSON.version); function log(msg: string) { console.log(`[${formatISO(new Date())}] ${msg}`); } function warn(msg: string) { console.error(`[${formatISO(new Date())}] ${colors.yellow(msg)}`); } function err(msg: string) { console.error(`[${formatISO(new Date())}] ${colors.red(msg)}`); } program .command("check") .description("Check a SVG file for common errors.") .argument( "[path to SVG file]", "Path to the LaS SVG file.", "../public/content/intro.svg" ) .action((fileName: string) => { log(`Loading "${fileName}"`); const fileContents = fs.readFileSync(fileName, { encoding: "utf8" }); const root = parse(fileContents); const TAGS = ["image", "rect", "circle", "ellipse", "a"]; const elements: { [key: string]: ElementNode[] } = {}; TAGS.forEach((tag) => (elements[tag] = [])); function walkAndSort(element: RootNode | ElementNode) { if ("children" in element) { element.children.forEach((child) => { if (typeof child !== "string" && "tagName" in child) { const tagName = child.tagName || ""; if (TAGS.includes(tagName)) { elements[tagName].push(child); } walkAndSort(child); } }); } } walkAndSort(root); const anchorLinks: ElementNode[] = elements["a"].filter((el) => { const href = String((el.properties || {})["xlink:href"] || ""); return !href.startsWith("http") && href.length > 0; }); log(`Found ${anchorLinks.length} links to anchors.`); const validTargets = elements["rect"].map( (el) => el.properties!.id as string ); anchorLinks.forEach((el) => { const href = String((el.properties || {})["xlink:href"] || ""); if (!validTargets.includes(href)) { const child = el.children.find((el) => typeof el !== "string") as | ElementNode | undefined; const childProps = child?.properties || {}; const cx = childProps["x"] || childProps["cx"] || "???"; const cy = childProps["y"] || childProps["cy"] || "???"; warn( ` - Link "${href}" (cx: ${cx}, cy: ${cy}) has no corresponding target object!` ); } }); // const badAudios = elements["ellipse"].filter((el) => { // if // }) // TODO }); program .command("process") .description("Process a directory of media content.") .argument("", "Path to the input directory.") .argument("", "Path to the output directory.") .option("-c, --clean", "Clean output directory.") .action( async ( inputDir: string, outputDir: string, options: { clean: boolean } ) => { const images: klaw.Item[] = []; const audios: klaw.Item[] = []; log(`Processing "${inputDir}"...`); for await (const item of klaw(inputDir)) { if (!item.stats.isFile()) { continue; } const fileType = await fileTypeFromFile(item.path); if (fileType?.mime?.startsWith("image")) { images.push(item); } if (fileType?.mime?.startsWith("audio")) { audios.push(item); } } log(`Found ${images.length} images and ${audios.length} audios.`); if (options.clean && fs.existsSync(outputDir)) { warn(`Deleting "${outputDir}"!`); fs.rmSync(outputDir, { recursive: true }); } const tmpDir = os.tmpdir(); try { log("Converting all images..."); const inputPath = path.resolve(inputDir); const outputPath = fs.mkdtempSync(`${tmpDir}${path.sep}las_`); const imagesBar = new cliProgress.SingleBar( {}, cliProgress.Presets.shades_classic ); imagesBar.start(images.length, 0); await async.eachLimit(images, os.cpus().length, async (image, cb) => { const fullPath = path.resolve(image.path); const relPath = fullPath.substring(inputPath.length + 1); const destPath = path.join(outputPath, relPath); const destDirPath = path.dirname(destPath); const parsedPath = path.parse(fullPath); fs.mkdirSync(destDirPath, { recursive: true }); // log(`Processing ${relPath}`); const lossless = fullPath.endsWith("png"); const ext = lossless ? "png" : "jpg"; let copy = sharp(fullPath); copy = lossless ? copy.png() : copy.jpeg(); await copy.toFile( path.join(destDirPath, `${parsedPath.name}.${ext}`) ); // log(`Finished copying ${relPath}`); let small = sharp(fullPath).resize(256); small = lossless ? small.png() : small.jpeg(); await small.toFile( path.join(destDirPath, `${parsedPath.name}_256.${ext}`) ); // log(`Finished resizing ${relPath}`); imagesBar.increment(); cb(); }); imagesBar.stop(); log("Optimizing all images..."); // TODO } finally { fs.rmSync(tmpDir, { recursive: true }); } } ); program.parse();