favgen 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/bin/index.js +40 -0
- package/lib/generator.d.ts +2 -0
- package/lib/generator.js +140 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
This is a simple CLI tool to generate an optimized set of favicons from a single input file. Icons are optimized in terms of both size and quantity (nowadays you don't that many of them). They are produced according to [this article](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs) which served as an inspiration for the tool.
|
|
4
|
+
|
|
5
|
+
You can provide a file with any extension that [sharp library](https://sharp.pixelplumbing.com/) accepts.
|
|
6
|
+
|
|
7
|
+
By default, the following set of favicons is produced:
|
|
8
|
+
- `favicon.svg` if input file was SVG and `favicon.png` 32x32 otherwise
|
|
9
|
+
- `favicon.ico` 32x32
|
|
10
|
+
- `favicon-192.png` 192x192 (for Android devices)
|
|
11
|
+
- `favicon-512.png` 192x192 (for Android devices)
|
|
12
|
+
- `apple-touch-icon.png` 180x180 (original image is resized to 140x140 and 20px padding transparent padding is added on each side; rationale is given in the article)
|
|
13
|
+
|
|
14
|
+
Additionally, a sample `manifest.webmanifest` file is produced which shows how favicons for Android devices are supposed to be included.
|
|
15
|
+
|
|
16
|
+
Besides that, PNG output is optimized by `sharp` (which uses `pngquant`) and SVG output is optimized by [SVGO](https://github.com/svg/svgo).
|
|
17
|
+
Also, color palette is reduced to 64 colors by default in order to reduce assets’ size.
|
|
18
|
+
|
|
19
|
+
You can tweak the following settings by providing additional commands:
|
|
20
|
+
- output directory by setting `-o` option (`__favicon__` by default)
|
|
21
|
+
- icon prefix (`favicon` by default)
|
|
22
|
+
- colors palette size by providing `--colors` followed by a number between 2 and 256
|
|
23
|
+
- producing 16x16 .ico file by providing `--include16` flag
|
|
24
|
+
|
|
25
|
+
The tool can also be used as an API:
|
|
26
|
+
```js
|
|
27
|
+
const { produceIcons } = require("favgen")
|
|
28
|
+
const inputFilePath = "favicon.svg"
|
|
29
|
+
const outputDirPath = "__favicons__"
|
|
30
|
+
const prefix = "blueberry"
|
|
31
|
+
const paletteSize = 64
|
|
32
|
+
const include16 = true
|
|
33
|
+
produceIcons(inputFilePath, outputDirPath, prefix, paletteSize, include16)
|
|
34
|
+
```
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { Command, InvalidArgumentError } = require("commander");
|
|
5
|
+
const PKG = require("../package.json");
|
|
6
|
+
const { produceIcons } = require("../lib");
|
|
7
|
+
|
|
8
|
+
const CWD = process.cwd();
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program.name(PKG.name).description(PKG.description).version(PKG.version);
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.description("Produce a set of favicons from a single input file.")
|
|
16
|
+
.argument("<inputPath>", "Input icon path")
|
|
17
|
+
.option("-o, --output <path>", "Output directory path", "__favicons__")
|
|
18
|
+
.option("--prefix <name>", "Icon prefix", "favicon")
|
|
19
|
+
.option("--colors <number>", "Color paleete size, between 2 and 256", 64)
|
|
20
|
+
.option("--include16", "Produce 16x16 .ico file", false)
|
|
21
|
+
.action((filepath, { output: outputDir, prefix, colors, include16 }) => {
|
|
22
|
+
const colorsPaletteSize = parseInt(colors, 10);
|
|
23
|
+
const isValidPaletteSize =
|
|
24
|
+
Number.isNaN(colorsPaletteSize) ||
|
|
25
|
+
colorsPaletteSize < 2 ||
|
|
26
|
+
colorsPaletteSize > 256;
|
|
27
|
+
if (isValidPaletteSize) {
|
|
28
|
+
throw new InvalidArgumentError(
|
|
29
|
+
"Color palette size must be a number between 2 and 256.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const inputPath = path.join(CWD, filepath);
|
|
34
|
+
const outputPath = path.isAbsolute(outputDir)
|
|
35
|
+
? outputDir
|
|
36
|
+
: path.join(CWD, outputDir);
|
|
37
|
+
produceIcons(inputPath, outputPath, prefix, colorsPaletteSize, include16);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program.parse();
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
9
|
+
const to_ico_1 = __importDefault(require("to-ico"));
|
|
10
|
+
const svgo_1 = require("svgo");
|
|
11
|
+
const is_svg_1 = __importDefault(require("is-svg"));
|
|
12
|
+
const baseIconConfigs = [
|
|
13
|
+
{
|
|
14
|
+
name: "favicon.svg",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
// no colors palette size otherwise its color profile is off
|
|
18
|
+
// which causes problems with converting to ico
|
|
19
|
+
name: "favicon.ico",
|
|
20
|
+
pxSize: 32,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "favicon-192.png",
|
|
24
|
+
pxSize: 192,
|
|
25
|
+
colorsPaletteSize: 64,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "favicon-512.png",
|
|
29
|
+
pxSize: 512,
|
|
30
|
+
colorsPaletteSize: 64,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "apple-touch-icon.png",
|
|
34
|
+
pxSize: 180,
|
|
35
|
+
colorsPaletteSize: 64,
|
|
36
|
+
padding: 20,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
function buildPng(rawBuffer, { pxSize, colorsPaletteSize, padding }) {
|
|
40
|
+
const outputIcon = (0, sharp_1.default)(rawBuffer)
|
|
41
|
+
.resize(pxSize, pxSize)
|
|
42
|
+
.png({ compressionLevel: 9, colors: colorsPaletteSize });
|
|
43
|
+
return padding
|
|
44
|
+
? outputIcon.extend({
|
|
45
|
+
top: padding,
|
|
46
|
+
left: padding,
|
|
47
|
+
bottom: padding,
|
|
48
|
+
right: padding,
|
|
49
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
50
|
+
})
|
|
51
|
+
: outputIcon;
|
|
52
|
+
}
|
|
53
|
+
function getPngBuffer(rawBuffer, cfg) {
|
|
54
|
+
return buildPng(rawBuffer, cfg).toBuffer();
|
|
55
|
+
}
|
|
56
|
+
function getIcoBuffer(rawBuffer, cfg) {
|
|
57
|
+
return buildPng(rawBuffer, cfg)
|
|
58
|
+
.toBuffer()
|
|
59
|
+
.then((buf) => (0, to_ico_1.default)(buf));
|
|
60
|
+
}
|
|
61
|
+
function getSvgBuffer(rawBuffer) {
|
|
62
|
+
const optimizedSvg = (0, svgo_1.optimize)(rawBuffer, {
|
|
63
|
+
multipass: true,
|
|
64
|
+
});
|
|
65
|
+
if (optimizedSvg.error !== undefined) {
|
|
66
|
+
throw Error(optimizedSvg.error);
|
|
67
|
+
}
|
|
68
|
+
return Buffer.from(optimizedSvg.data);
|
|
69
|
+
}
|
|
70
|
+
function getIconBuffer(rawBuffer, cfg) {
|
|
71
|
+
const iconName = cfg.name;
|
|
72
|
+
const iconExtension = path_1.default.extname(iconName);
|
|
73
|
+
switch (iconExtension) {
|
|
74
|
+
case ".svg":
|
|
75
|
+
return getSvgBuffer(rawBuffer);
|
|
76
|
+
case ".png":
|
|
77
|
+
return getPngBuffer(rawBuffer, cfg);
|
|
78
|
+
case ".ico":
|
|
79
|
+
return getIcoBuffer(rawBuffer, cfg);
|
|
80
|
+
default:
|
|
81
|
+
throw Error(`Extension ${iconExtension} is not recognized.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function produceIcons(inputFilePath, outputDirPath, prefix = "favicon", paletteSize = 64, include16 = false) {
|
|
85
|
+
await promises_1.default.access(inputFilePath);
|
|
86
|
+
try {
|
|
87
|
+
await promises_1.default.access(outputDirPath);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (e instanceof Error && e.message.includes("ENOENT")) {
|
|
91
|
+
promises_1.default.mkdir(outputDirPath);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const rawIconBuf = await promises_1.default.readFile(inputFilePath);
|
|
98
|
+
const isSvgBuf = (0, is_svg_1.default)(rawIconBuf);
|
|
99
|
+
let iconConfigs = baseIconConfigs.map((cfg) => {
|
|
100
|
+
const mappedCfg = { ...cfg };
|
|
101
|
+
if (!isSvgBuf && mappedCfg.name.endsWith(".svg")) {
|
|
102
|
+
mappedCfg.name = mappedCfg.name.replace(".svg", ".png");
|
|
103
|
+
mappedCfg.pxSize = 32;
|
|
104
|
+
}
|
|
105
|
+
mappedCfg.name = mappedCfg.name.replace("favicon", prefix);
|
|
106
|
+
if (mappedCfg.name.endsWith(".png")) {
|
|
107
|
+
mappedCfg.colorsPaletteSize = paletteSize;
|
|
108
|
+
}
|
|
109
|
+
return mappedCfg;
|
|
110
|
+
});
|
|
111
|
+
if (include16) {
|
|
112
|
+
const ico = iconConfigs.find((cfg) => cfg.name.endsWith(".ico"));
|
|
113
|
+
const icoNameWithoutExt = ico.name.slice(0, -4);
|
|
114
|
+
iconConfigs.push({ ...ico, name: `${icoNameWithoutExt}-32.ico` });
|
|
115
|
+
iconConfigs.push({
|
|
116
|
+
...ico,
|
|
117
|
+
name: `${icoNameWithoutExt}-16.ico`,
|
|
118
|
+
pxSize: 16,
|
|
119
|
+
});
|
|
120
|
+
iconConfigs = iconConfigs.filter((cfg) => cfg !== ico);
|
|
121
|
+
}
|
|
122
|
+
const iconsGenerationSeries = iconConfigs.map(async (cfg) => {
|
|
123
|
+
const iconName = cfg.name.replace("favicon", prefix);
|
|
124
|
+
const iconCfg = cfg.name.endsWith("png")
|
|
125
|
+
? { ...cfg, colorsPaletteSize: paletteSize, name: iconName }
|
|
126
|
+
: { ...cfg, name: iconName };
|
|
127
|
+
const outputBuffer = await getIconBuffer(rawIconBuf, iconCfg);
|
|
128
|
+
return promises_1.default.writeFile(path_1.default.join(outputDirPath, iconCfg.name), outputBuffer);
|
|
129
|
+
});
|
|
130
|
+
await Promise.all(iconsGenerationSeries);
|
|
131
|
+
const manifestFile = {
|
|
132
|
+
icons: [
|
|
133
|
+
{ src: `/${prefix}-192.png`, type: "image/png", sizes: "192x192" },
|
|
134
|
+
{ src: `/${prefix}-512.png`, type: "image/png", sizes: "512x512" },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const manifestText = JSON.stringify(manifestFile, null, 2);
|
|
138
|
+
promises_1.default.writeFile(path_1.default.join(outputDirPath, "manifest.webmanifest"), manifestText);
|
|
139
|
+
}
|
|
140
|
+
exports.default = produceIcons;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as produceIcons } from "./generator";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.produceIcons = void 0;
|
|
7
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
8
|
+
var generator_1 = require("./generator");
|
|
9
|
+
Object.defineProperty(exports, "produceIcons", { enumerable: true, get: function () { return __importDefault(generator_1).default; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "favgen",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI tool to generate a set of favicons from a single input file.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"favicon"
|
|
7
|
+
],
|
|
8
|
+
"author": "islambeg",
|
|
9
|
+
"license": "Unlicense",
|
|
10
|
+
"homepage": "https://github.com/favgen/favgen-cli",
|
|
11
|
+
"repository": "https://github.com/favgen/favgen-cli.git",
|
|
12
|
+
"main": "lib/index.js",
|
|
13
|
+
"types": "lib/index.d.ts",
|
|
14
|
+
"bin": "bin/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"lib/",
|
|
17
|
+
"bin/"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">= 16"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc --project tsconfig.json",
|
|
24
|
+
"build:dev": "tsc --watch --project tsconfig.json",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"preinstall": "husky install && husky add .husky/pre-commit 'npx lint-staged' && git add .husky/pre-commit",
|
|
27
|
+
"postinstall": "npm run build",
|
|
28
|
+
"lint": "eslint --ext .js,.cjs,.mjs,.ts,.cts,.mts --fix --ignore-path .gitignore --cache",
|
|
29
|
+
"prettify": "prettier --write --ignore-path .gitignore --plugin-search-dir=."
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^9.4.0",
|
|
33
|
+
"is-svg": "^4.3.2",
|
|
34
|
+
"sharp": "^0.30.7",
|
|
35
|
+
"svgo": "^2.8.0",
|
|
36
|
+
"to-ico": "^1.1.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@tsconfig/node16": "^1.0.3",
|
|
40
|
+
"@types/sharp": "^0.30.4",
|
|
41
|
+
"@types/svgo": "^2.6.3",
|
|
42
|
+
"@types/to-ico": "^1.1.1",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
|
44
|
+
"@typescript-eslint/parser": "^5.30.7",
|
|
45
|
+
"eslint": "^8.20.0",
|
|
46
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
47
|
+
"eslint-config-airbnb-typescript": "^17.0.0",
|
|
48
|
+
"eslint-config-prettier": "^8.5.0",
|
|
49
|
+
"eslint-plugin-import": "^2.26.0",
|
|
50
|
+
"husky": "^8.0.1",
|
|
51
|
+
"lint-staged": "^13.0.3",
|
|
52
|
+
"prettier": "^2.7.1",
|
|
53
|
+
"typescript": "^4.7.4"
|
|
54
|
+
}
|
|
55
|
+
}
|