wip: add tools

wip/tools
Tomáš Mládek 2022-07-31 18:01:55 +02:00
parent a5f1846491
commit 1c8e8129a5
4 changed files with 2574 additions and 0 deletions

192
tools/main.ts Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true
}
}

2341
tools/yarn.lock Normal file

File diff suppressed because it is too large Load Diff