Node.js script to convert an SVG to an ico for maximum favicon compatibility
0
svg-to-ico.js
154 lines 4.5 kB view raw
1#!/usr/bin/env node 2 3/** 4 * SVG to ICO Converter 5 * 6 * Converts SVG files to ICO format with multiple sizes 7 * 8 * 9 * Dependencies: 10 * chalk (v5.6.2): https://www.npmjs.com/package/chalk 11 * png-to-ico (3.0.1): https://www.npmjs.com/package/png-to-ico 12 * sharp (0.34.5): https://www.npmjs.com/package/sharp 13 */ 14 15import chalk from "chalk"; 16import { access, mkdir, readFile, readdir, writeFile } from "fs/promises"; 17import { basename, dirname, extname, join } from "path"; 18import pngToIco from "png-to-ico"; 19import sharp from "sharp"; 20import { fileURLToPath } from "url"; 21 22const __filename = fileURLToPath(import.meta.url); 23const __dirname = dirname(__filename); 24 25/** ICO sizes commonly used for favicons (must include 16x16 and 32x32 for best compatibility) */ 26const ICO_SIZES = [16, 32, 48, 64, 128, 256]; 27 28/** 29 * Check if a path exists 30 */ 31async function pathExists(path) { 32 try { 33 await access(path); 34 return true; 35 } catch { 36 return false; 37 } 38} 39 40/** 41 * Convert SVG buffer to PNG buffers at specified sizes 42 */ 43async function svgToPngBuffers(svgBuffer, sizes) { 44 const pngBuffers = []; 45 46 for (const size of sizes) { 47 const pngBuffer = await sharp(svgBuffer, { density: 300 }) 48 .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) 49 .png({ compressionLevel: 9, adaptiveFiltering: true, force: true }) 50 .toBuffer(); 51 pngBuffers.push(pngBuffer); 52 } 53 54 return pngBuffers; 55} 56 57/** 58 * Convert a single SVG file to ICO 59 */ 60async function convertSvgToIco(inputPath, outputPath) { 61 console.log(chalk.blue(`Converting: ${inputPath}`)); 62 63 const svgBuffer = await readFile(inputPath); 64 const pngBuffers = await svgToPngBuffers(svgBuffer, ICO_SIZES); 65 const icoBuffer = await pngToIco(pngBuffers); 66 67 const outputDir = dirname(outputPath); 68 if (!(await pathExists(outputDir))) { 69 await mkdir(outputDir, { recursive: true }); 70 } 71 72 await writeFile(outputPath, icoBuffer); 73 74 console.log(chalk.green(`✓ Created: ${outputPath}`)); 75 console.log(chalk.gray(` Sizes: ${ICO_SIZES.join(", ")}px`)); 76 console.log(chalk.gray(` File size: ${(icoBuffer.length / 1024).toFixed(1)}KB`)); 77} 78 79/** 80 * Process all SVG files in a directory 81 */ 82async function processDirectory(inputDir, outputDir) { 83 const files = await readdir(inputDir); 84 const svgFiles = files.filter((file) => extname(file).toLowerCase() === ".svg"); 85 86 if (svgFiles.length === 0) { 87 console.log(chalk.yellow(`No SVG files found in ${inputDir}`)); 88 return; 89 } 90 91 console.log(chalk.cyan(`Found ${svgFiles.length} SVG file(s) to convert\n`)); 92 93 for (const file of svgFiles) { 94 const inputPath = join(inputDir, file); 95 const outputFile = `${basename(file, ".svg")}.ico`; 96 const outputPath = join(outputDir, outputFile); 97 98 try { 99 await convertSvgToIco(inputPath, outputPath); 100 } catch (error) { 101 console.error(chalk.red(`✗ Failed to convert ${file}:`), error.message); 102 } 103 } 104} 105 106/** 107 * Main function 108 */ 109async function main() { 110 const args = process.argv.slice(2); 111 112 if (args.includes("--help") || args.includes("-h")) { 113 console.log(chalk.cyan("SVG to ICO Converter\n")); 114 console.log("Usage:"); 115 console.log(" node convert-svg-to-ico.js [input] [output]\n"); 116 console.log("Arguments:"); 117 console.log(" input - Input SVG file or directory (default: ./public)"); 118 console.log(" output - Output ICO file or directory (default: same as input)\n"); 119 console.log("Examples:"); 120 console.log(" node convert-svg-to-ico.js"); 121 console.log(" node convert-svg-to-ico.js ./public/favicon.svg"); 122 console.log(" node convert-svg-to-ico.js ./assets ./dist\n"); 123 return; 124 } 125 126 const input = args[0] || "./public"; 127 const output = args[1]; 128 129 try { 130 const inputExists = await pathExists(input); 131 if (!inputExists) { 132 console.error(chalk.red(`Error: Input path does not exist: ${input}`)); 133 process.exit(1); 134 } 135 136 const isFile = (await readFile(input).catch(() => null)) !== null && input.toLowerCase().endsWith(".svg"); 137 138 if (isFile) { 139 const outputPath = output || input.replace(/\.svg$/i, ".ico"); 140 await convertSvgToIco(input, outputPath); 141 } else { 142 const outputDir = output || input; 143 await processDirectory(input, outputDir); 144 } 145 146 console.log(chalk.green("\n✓ Conversion complete!")); 147 } catch (error) { 148 console.error(chalk.red("\n✗ Error:"), error.message); 149 console.error(chalk.gray(error.stack)); 150 process.exit(1); 151 } 152} 153 154main();