diginext-img-magic-cli
Version:
README.md
320 lines ⢠13.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const yargs_1 = __importDefault(require("yargs"));
const promises_1 = __importDefault(require("fs/promises"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const async_1 = require("async");
const diginext_img_magic_1 = require("diginext-img-magic");
const isImageSupported_1 = __importDefault(require("diginext-img-magic/dist/compress/isImageSupported"));
const url_1 = require("diginext-utils/dist/string/url");
const getAllFiles_1 = __importDefault(require("./plugins/getAllFiles"));
const { version } = require("../package.json");
class ImageConverter {
constructor(options) {
this.outputDirWebp = "";
this.outputDirThumb = "";
this.results = [];
this.processedCount = 0;
this.options = options;
this.setupOutputDirectories();
}
setupOutputDirectories() {
const normalizedDir = this.options.dir.replace(/\\/g, "/");
const folderName = (0, url_1.getFileNameWithoutExtension)(normalizedDir);
const parentDir = path_1.default.dirname(normalizedDir);
this.outputDirWebp = path_1.default.join(parentDir, `${folderName}-webp`);
this.outputDirThumb = path_1.default.join(parentDir, `${folderName}-thumb-webp`);
}
async ensureDirectoryExists(dirPath) {
try {
if (fs_1.default.existsSync(dirPath)) {
await promises_1.default.rm(dirPath, { recursive: true });
}
await promises_1.default.mkdir(dirPath, { recursive: true });
}
catch (error) {
throw new Error(`Failed to create directory ${dirPath}: ${error}`);
}
}
async validateDirectory() {
try {
const stats = await promises_1.default.lstat(this.options.dir);
if (!stats.isDirectory()) {
throw new Error("Path is not a directory");
}
}
catch (error) {
throw new Error(`Invalid directory: ${this.options.dir}`);
}
}
checkForDuplicateNames(files) {
const nameMap = new Map();
files.forEach((file) => {
if ((0, isImageSupported_1.default)(file)) {
const nameWithoutExt = (0, url_1.getFileNameWithoutExtension)(file);
const relativePath = path_1.default.dirname(file.replace(this.options.dir, ""));
const key = path_1.default.join(relativePath, nameWithoutExt);
if (!nameMap.has(key)) {
nameMap.set(key, []);
}
nameMap.get(key).push(file);
}
});
const duplicates = [];
nameMap.forEach((files, name) => {
if (files.length > 1) {
duplicates.push(...files);
console.warn(`ā ļø Duplicate names found for: ${name}`);
console.warn(` Files: ${files.join(", ")}`);
}
});
return duplicates;
}
async convertSingleImage(task) {
const { inputPath, index, total } = task;
try {
// console.log(`šø [${index}/${total}] Processing: ${path.basename(inputPath)}`);
const normalizedPath = inputPath.replace(/\\/g, "/");
const fileExt = (0, url_1.getFileExtension)(normalizedPath);
const isSupported = (0, isImageSupported_1.default)(normalizedPath);
// Handle non-image files
if (!isSupported) {
const outputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp);
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
await promises_1.default.copyFile(normalizedPath, outputPath);
return {
success: true,
input: inputPath,
output: outputPath,
};
}
// Convert image to WebP
const result = await (0, diginext_img_magic_1.convertToWebpByPath)(normalizedPath, {
maxSize: this.options.maxsize,
small: this.options.thumb,
});
// Handle main WebP file
const webpOutputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp).replace(`.${fileExt}`, ".webp");
await promises_1.default.mkdir(path_1.default.dirname(webpOutputPath), { recursive: true });
await promises_1.default.copyFile(result.large, webpOutputPath);
await promises_1.default.rm(result.large);
// Handle thumbnail if requested
if (this.options.thumb && result.small) {
const thumbOutputPath = normalizedPath.replace(this.options.dir, this.outputDirThumb).replace(`.${fileExt}`, ".webp");
await promises_1.default.mkdir(path_1.default.dirname(thumbOutputPath), { recursive: true });
await promises_1.default.copyFile(result.small, thumbOutputPath);
await promises_1.default.rm(result.small);
}
return {
success: true,
input: inputPath,
output: webpOutputPath,
};
}
catch (error) {
return {
success: false,
input: inputPath,
error: error instanceof Error ? error.message : String(error),
};
}
}
createWorker() {
return async (task) => {
try {
const result = await this.convertSingleImage(task);
this.results.push(result);
this.processedCount++;
if (!result.success) {
console.error(`ā Failed to convert ${task.inputPath}: ${result.error}`);
}
else {
console.log(`ā [${this.processedCount}/${task.total}] Completed: ${path_1.default.basename(task.inputPath)}`);
}
}
catch (error) {
const errorResult = {
success: false,
input: task.inputPath,
error: error instanceof Error ? error.message : String(error),
};
this.results.push(errorResult);
this.processedCount++;
console.error(`ā Worker error for ${task.inputPath}:`, error);
}
};
}
async processWithQueue(files) {
return new Promise((resolve, reject) => {
const concurrency = this.options.concurrency || 10;
console.log(`š Starting conversion of ${files.length} files with concurrency: ${concurrency}`);
// Create the queue with our worker function
const q = (0, async_1.queue)(this.createWorker(), concurrency);
// Set up queue event handlers
q.error((error, task) => {
console.error("Queue error occurred:", error);
console.error("Failed task:", task);
});
q.drain(() => {
console.log("\nšÆ All tasks completed!");
resolve(this.results);
});
// Add progress monitoring
// let lastProgress = 0;
// q.saturated(() => {
// console.log("š„ Queue is running at full capacity");
// });
q.empty(() => {
console.log("š Queue is empty, waiting for workers to finish...");
});
// Create tasks and add them to the queue
const tasks = files.map((file, index) => ({
inputPath: file,
index: index + 1,
total: files.length,
}));
// Add all tasks to the queue
q.push(tasks, (error) => {
if (error) {
console.error("Task completion error:", error);
}
});
// Handle the case where there are no files
if (files.length === 0) {
resolve([]);
}
});
}
async cleanup() {
try {
if (fs_1.default.existsSync(".temp")) {
await promises_1.default.rm(".temp", { recursive: true });
}
}
catch (error) {
console.warn("ā ļø Warning: Failed to cleanup temp directory:", error);
}
}
printSummary(results) {
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
console.log("\n" + "=".repeat(50));
console.log("š CONVERSION SUMMARY");
console.log("=".repeat(50));
console.log(`ā Successful: ${successful}`);
console.log(`ā Failed: ${failed}`);
console.log(`š Output directory: ${this.outputDirWebp}`);
if (this.options.thumb) {
console.log(`š¼ļø Thumbnail directory: ${this.outputDirThumb}`);
}
if (failed > 0) {
console.log("\nā Failed conversions:");
results.filter((r) => !r.success).forEach((r) => console.log(` ⢠${path_1.default.basename(r.input)}: ${r.error}`));
}
console.log("=".repeat(50));
}
async convert() {
try {
// Reset state
this.results = [];
this.processedCount = 0;
// Validate input directory
await this.validateDirectory();
// Get all files
const files = (0, getAllFiles_1.default)(this.options.dir);
if (files.length === 0) {
console.log("š No files found in the specified directory.");
return;
}
// Check for duplicate names
const duplicates = this.checkForDuplicateNames(files);
if (duplicates.length > 0) {
throw new Error("Cannot proceed: Found files with duplicate names but different extensions.");
}
// Setup output directories
await this.ensureDirectoryExists(this.outputDirWebp);
if (this.options.thumb) {
await this.ensureDirectoryExists(this.outputDirThumb);
}
// Process files using async queue
const results = await this.processWithQueue(files);
// Cleanup and summary
await this.cleanup();
this.printSummary(results);
}
catch (error) {
console.error("š„ Conversion failed:", error instanceof Error ? error.message : String(error));
throw error;
}
}
}
async function parseArguments() {
const argv = await (0, yargs_1.default)(process.argv.slice(2))
.usage("Usage: $0 [options]")
.example('$0 --dir "./images" --thumb --maxsize 2048 --concurrency 5', "Convert images with 5 concurrent workers")
.option("dir", {
alias: "d",
describe: "Source directory containing images",
type: "string",
demandOption: true,
})
.option("thumb", {
alias: "t",
describe: "Generate thumbnails",
type: "boolean",
default: false,
})
.option("maxsize", {
alias: "m",
describe: "Maximum size for converted images",
type: "number",
default: 4096,
})
.option("concurrency", {
alias: "c",
describe: "Number of concurrent workers",
type: "number",
default: 12,
})
.help("h")
.alias("h", "help")
.epilog(`Version: ${version}`)
.parseAsync();
return {
dir: argv.dir,
thumb: argv.thumb,
maxsize: argv.maxsize,
concurrency: argv.concurrency,
};
}
async function main() {
try {
console.log(`šØ Image Converter v${version}`);
console.log("=".repeat(30));
const options = await parseArguments();
console.log(`š Source: ${options.dir}`);
console.log(`š Max size: ${options.maxsize}px`);
console.log(`š¼ļø Generate thumbnails: ${options.thumb ? "Yes" : "No"}`);
console.log(`ā” Concurrency: ${options.concurrency}`);
console.log("=".repeat(30));
const converter = new ImageConverter(options);
await converter.convert();
console.log("š All done!");
}
catch (error) {
console.error("š„ Application error:", error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
// Run the application
main();
//# sourceMappingURL=index.js.map