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