#!/usr/bin/env node /** * SVG to ICO Converter * * Converts SVG files to ICO format with multiple sizes * * * Dependencies: * chalk (v5.6.2): https://www.npmjs.com/package/chalk * png-to-ico (3.0.1): https://www.npmjs.com/package/png-to-ico * sharp (0.34.5): https://www.npmjs.com/package/sharp */ import chalk from "chalk"; import { access, mkdir, readFile, readdir, writeFile } from "fs/promises"; import { basename, dirname, extname, join } from "path"; import pngToIco from "png-to-ico"; import sharp from "sharp"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** ICO sizes commonly used for favicons (must include 16x16 and 32x32 for best compatibility) */ const ICO_SIZES = [16, 32, 48, 64, 128, 256]; /** * Check if a path exists */ async function pathExists(path) { try { await access(path); return true; } catch { return false; } } /** * Convert SVG buffer to PNG buffers at specified sizes */ async function svgToPngBuffers(svgBuffer, sizes) { const pngBuffers = []; for (const size of sizes) { const pngBuffer = await sharp(svgBuffer, { density: 300 }) .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) .png({ compressionLevel: 9, adaptiveFiltering: true, force: true }) .toBuffer(); pngBuffers.push(pngBuffer); } return pngBuffers; } /** * Convert a single SVG file to ICO */ async function convertSvgToIco(inputPath, outputPath) { console.log(chalk.blue(`Converting: ${inputPath}`)); const svgBuffer = await readFile(inputPath); const pngBuffers = await svgToPngBuffers(svgBuffer, ICO_SIZES); const icoBuffer = await pngToIco(pngBuffers); const outputDir = dirname(outputPath); if (!(await pathExists(outputDir))) { await mkdir(outputDir, { recursive: true }); } await writeFile(outputPath, icoBuffer); console.log(chalk.green(`āœ“ Created: ${outputPath}`)); console.log(chalk.gray(` Sizes: ${ICO_SIZES.join(", ")}px`)); console.log(chalk.gray(` File size: ${(icoBuffer.length / 1024).toFixed(1)}KB`)); } /** * Process all SVG files in a directory */ async function processDirectory(inputDir, outputDir) { const files = await readdir(inputDir); const svgFiles = files.filter((file) => extname(file).toLowerCase() === ".svg"); if (svgFiles.length === 0) { console.log(chalk.yellow(`No SVG files found in ${inputDir}`)); return; } console.log(chalk.cyan(`Found ${svgFiles.length} SVG file(s) to convert\n`)); for (const file of svgFiles) { const inputPath = join(inputDir, file); const outputFile = `${basename(file, ".svg")}.ico`; const outputPath = join(outputDir, outputFile); try { await convertSvgToIco(inputPath, outputPath); } catch (error) { console.error(chalk.red(`āœ— Failed to convert ${file}:`), error.message); } } } /** * Main function */ async function main() { const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { console.log(chalk.cyan("SVG to ICO Converter\n")); console.log("Usage:"); console.log(" node convert-svg-to-ico.js [input] [output]\n"); console.log("Arguments:"); console.log(" input - Input SVG file or directory (default: ./public)"); console.log(" output - Output ICO file or directory (default: same as input)\n"); console.log("Examples:"); console.log(" node convert-svg-to-ico.js"); console.log(" node convert-svg-to-ico.js ./public/favicon.svg"); console.log(" node convert-svg-to-ico.js ./assets ./dist\n"); return; } const input = args[0] || "./public"; const output = args[1]; try { const inputExists = await pathExists(input); if (!inputExists) { console.error(chalk.red(`Error: Input path does not exist: ${input}`)); process.exit(1); } const isFile = (await readFile(input).catch(() => null)) !== null && input.toLowerCase().endsWith(".svg"); if (isFile) { const outputPath = output || input.replace(/\.svg$/i, ".ico"); await convertSvgToIco(input, outputPath); } else { const outputDir = output || input; await processDirectory(input, outputDir); } console.log(chalk.green("\nāœ“ Conversion complete!")); } catch (error) { console.error(chalk.red("\nāœ— Error:"), error.message); console.error(chalk.gray(error.stack)); process.exit(1); } } main();