UNPKG

diginext-img-magic-cli

Version:
315 lines • 12.6 kB
import yargs from "yargs"; import fs from "fs/promises"; import fsSync from "fs"; import path from "path"; import { queue } from "async"; import { convertToWebpByPath } from "diginext-img-magic"; import isImageSupported from "diginext-img-magic/dist/compress/isImageSupported"; import { getFileExtension, getFileNameWithoutExtension } from "diginext-utils/dist/string/url"; import getAllFiles from "./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 = getFileNameWithoutExtension(normalizedDir); const parentDir = path.dirname(normalizedDir); this.outputDirWebp = path.join(parentDir, `${folderName}-webp`); this.outputDirThumb = path.join(parentDir, `${folderName}-thumb-webp`); } async ensureDirectoryExists(dirPath) { try { if (fsSync.existsSync(dirPath)) { await fs.rm(dirPath, { recursive: true }); } await fs.mkdir(dirPath, { recursive: true }); } catch (error) { throw new Error(`Failed to create directory ${dirPath}: ${error}`); } } async validateDirectory() { try { const stats = await fs.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 (isImageSupported(file)) { const nameWithoutExt = getFileNameWithoutExtension(file); const relativePath = path.dirname(file.replace(this.options.dir, "")); const key = path.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 = getFileExtension(normalizedPath); const isSupported = isImageSupported(normalizedPath); // Handle non-image files if (!isSupported) { const outputPath = normalizedPath.replace(this.options.dir, this.outputDirWebp); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.copyFile(normalizedPath, outputPath); return { success: true, input: inputPath, output: outputPath, }; } // Convert image to WebP const result = await 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 fs.mkdir(path.dirname(webpOutputPath), { recursive: true }); await fs.copyFile(result.large, webpOutputPath); await fs.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 fs.mkdir(path.dirname(thumbOutputPath), { recursive: true }); await fs.copyFile(result.small, thumbOutputPath); await fs.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.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 = 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 (fsSync.existsSync(".temp")) { await fs.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.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 = getAllFiles(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 yargs(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