wip: add tools
This commit is contained in:
parent
a5f1846491
commit
1c8e8129a5
4 changed files with 2574 additions and 0 deletions
192
tools/main.ts
Normal file
192
tools/main.ts
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
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("<input>", "Path to the input directory.")
|
||||||
|
.argument("<output>", "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();
|
33
tools/package.json
Normal file
33
tools/package.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "las-tools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Tools for LaS authoring.",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"tools": "ts-node --esm main.ts"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"cli-progress": "^3.11.2",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"commander": "^9.4.0",
|
||||||
|
"date-fns": "^2.29.1",
|
||||||
|
"file-type": "^17.1.4",
|
||||||
|
"imagemin": "^8.0.1",
|
||||||
|
"imagemin-jpegtran": "^7.0.0",
|
||||||
|
"imagemin-pngquant": "^9.0.2",
|
||||||
|
"klaw": "^4.0.1",
|
||||||
|
"sharp": "^0.30.7",
|
||||||
|
"svg-parser": "^2.0.4",
|
||||||
|
"ts-node": "^10.9.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/async": "^3.2.15",
|
||||||
|
"@types/cli-progress": "^3.11.0",
|
||||||
|
"@types/klaw": "^3.0.3",
|
||||||
|
"@types/node": "^18.6.3",
|
||||||
|
"@types/sharp": "^0.30.4"
|
||||||
|
}
|
||||||
|
}
|
8
tools/tsconfig.json
Normal file
8
tools/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
2341
tools/yarn.lock
Normal file
2341
tools/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue