Node.js script to convert an SVG to an ico for maximum favicon compatibility
0
svg-to-ico.js
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();