├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── README.md ├── clean.js ├── data │ ├── 1024.png │ ├── 120.png │ ├── 128.png │ ├── 144.png │ ├── 152.png │ ├── 16.png │ ├── 195.png │ ├── 228.png │ ├── 24.png │ ├── 256.png │ ├── 32.png │ ├── 48.png │ ├── 512.png │ ├── 57.png │ ├── 64.png │ ├── 72.png │ ├── 96.png │ └── sample.svg ├── filename.js ├── from-png.js ├── from-png.ts ├── from-svg.js ├── from-svg.ts ├── icons │ └── .gitkeep └── package.json ├── package.json ├── src ├── bin │ ├── cli.test.ts │ ├── cli.ts │ └── index.ts └── lib │ ├── favicon.test.ts │ ├── favicon.ts │ ├── icns.test.ts │ ├── icns.ts │ ├── ico.test.ts │ ├── ico.ts │ ├── index.ts │ ├── logger.ts │ ├── png.test.ts │ ├── png.ts │ ├── rle.test.ts │ └── rle.ts ├── tsconfig.json └── vitest.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Project 37 | dist/ 38 | .* 39 | *.tgz 40 | examples/icons/*.icns 41 | examples/icons/*.ico 42 | examples/icons/*.png 43 | package-lock.json 44 | !.gitignore 45 | !.prettierrc 46 | !.github/ 47 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## 5.0.0 4 | 5 | ### Breaking Changes 6 | 7 | - feat: [Breaking Change] Node.js v20 [#164](https://github.com/akabekobeko/npm-icon-gen/pull/164) 8 | 9 | ## 4.0.0 10 | 11 | ### Breaking Changes 12 | 13 | - feat: [Breaking Cgange] support Node.js v18 [#149](https://github.com/akabekobeko/npm-icon-gen/pull/149) 14 | 15 | ### Chore 16 | 17 | - Removed unnecessary uuid lib [#158](https://github.com/akabekobeko/npm-icon-gen/pull/158) by [panther7](https://github.com/panther7) 18 | 19 | ## 3.0.1 20 | 21 | ### Features 22 | 23 | - update sharp [#144](https://github.com/akabekobeko/npm-icon-gen/pull/144) by [jhicken](https://github.com/jhicken) 24 | 25 | ## 3.0.0 26 | 27 | ### Breaking Changes 28 | 29 | - Support Node.js v12 or later [#122](https://github.com/akabekobeko/npm-icon-gen/issues/122) 30 | - change to sharp [#136](https://github.com/akabekobeko/npm-icon-gen/pull/136) by [mifi (Mikael Finstad)](https://github.com/mifi) 31 | 32 | SVG files are rendering to PNG file in [sharp](https://www.npmjs.com/package/sharp). Rendering files is output to a temporary directory of the each OS. 33 | 34 | Stopped using [svg2png](https://www.npmjs.com/package/svg2png) because of its dependency on [phantomjs](https://www.npmjs.com/package/phantomjs), which is deprecated. 35 | 36 | The quality of PNG generated from SVG will change, so if you need the previous results, use icon-gen v2.1.0. 37 | 38 | ``` 39 | $ npm install icon-gen@2.1.0 40 | ``` 41 | 42 | In the future, I may add SVG to PNG conversion by Chromium via [puppeteer-core](https://www.npmjs.com/package/puppeteer-core) in addition to [sharp](https://www.npmjs.com/package/sharp). 43 | 44 | ## 2.1.0 45 | 46 | ### Breaking Changes 47 | 48 | Please be sure to read the `README` because there are many changes. 49 | 50 | - Drop Node 8 51 | - Change CLI/Node options 52 | 53 | ### Features 54 | 55 | - To TypeScript and bundle `d.ts` file [#108](https://github.com/akabekobeko/npm-icon-gen/issues/108) 56 | 57 | ### Bug Fixes 58 | 59 | - Strict stream finish for ICNS/ICO generation [#126](https://github.com/akabekobeko/npm-icon-gen/issues/126) by [quanglam2807 (Quang Lam)](https://github.com/quanglam2807) 60 | - Delete file execution attribute [#113](https://github.com/akabekobeko/npm-icon-gen/issues/113) 61 | 62 | ### Documentation 63 | 64 | - Update README.md [#119](https://github.com/akabekobeko/npm-icon-gen/pull/119/files) by [rickysullivan (Ricky Sullivan Himself)](https://github.com/rickysullivan) 65 | - Remove "Node.js requirements" section from README.md [#114](https://github.com/akabekobeko/npm-icon-gen/pull/114) by [MakeNowJust (TSUYUSATO Kitsune)](https://github.com/MakeNowJust) 66 | 67 | ## v2.0.0 68 | 69 | ### Breaking Changes 70 | 71 | Please be sure to read the `README` because there are many changes. 72 | 73 | - Drop Node 6 [#101](https://github.com/akabekobeko/npm-icon-gen/issues/101) 74 | - Change CLI/Node options [#98](https://github.com/akabekobeko/npm-icon-gen/issues/98) 75 | 76 | ## Features 77 | 78 | - Support size specification of PNG (in Favicon) [#97](https://github.com/akabekobeko/npm-icon-gen/issues/97) 79 | 80 | ## v1.2.3 81 | 82 | ### Bug Fixes 83 | 84 | - Fix generating icons without specifying sizes [#94](https://github.com/akabekobeko/npm-icon-gen/pull/94) by [doug-a-brunner (Doug Brunner)](https://github.com/doug-a-brunner) 85 | 86 | ## v1.2.2 87 | 88 | ### Features 89 | 90 | - Support Node v10.x [#90](https://github.com/akabekobeko/npm-icon-gen/issues/90) 91 | 92 | ### Bug Fixes 93 | 94 | - CLI size specification is not working [#89](https://github.com/akabekobeko/npm-icon-gen/issues/89) 95 | 96 | ## v1.2.1 97 | 98 | ### Features 99 | 100 | - Support Node 9 [#87](https://github.com/akabekobeko/npm-icon-gen/issues/87) 101 | 102 | ## v1.2.0 103 | 104 | ### Features 105 | 106 | - Supports `is32` and `il32` [#71](https://github.com/akabekobeko/npm-icon-gen/issues/71) 107 | 108 | ## v1.1.5 109 | 110 | ### Bug Fixes 111 | 112 | - Fail if the `sizes` option is not specified [#66](https://github.com/akabekobeko/npm-icon-gen/issues/66) 113 | 114 | ## v1.1.4 115 | 116 | ### Features 117 | 118 | - add sizes option to define witch size of png to include [#62](https://github.com/akabekobeko/npm-icon-gen/pull/62) by [beijaflor (sho otani)](https://github.com/beijaflor) 119 | 120 | ## v1.1.3 121 | 122 | ### Bug Fixes 123 | 124 | - Close a write stream [#57](https://github.com/akabekobeko/npm-icon-gen/pull/57) by [satorf](https://github.com/satorf) 125 | - Close stream explicitly [#58](https://github.com/akabekobeko/npm-icon-gen/issues/58) 126 | 127 | ## v1.1.2 128 | 129 | ### Bug Fixes 130 | 131 | - npm install fails [#56](https://github.com/akabekobeko/npm-icon-gen/issues/56) 132 | 133 | ## v1.1.0 134 | 135 | ### Features 136 | 137 | - Using the Babel and change structure of project [#55](https://github.com/akabekobeko/npm-icon-gen/issues/55) 138 | - Add ICNS Retina support [#52](https://github.com/akabekobeko/npm-icon-gen/pull/52) by [quanglam2807 (Quang Lam)](https://github.com/quanglam2807) 139 | - **Drop the Node v4 (Breaking change)** [#51](https://github.com/akabekobeko/npm-icon-gen/issues/51) 140 | 141 | ### Bug Fixes 142 | 143 | - Icon can not be set as Finder's Folder [#54](https://github.com/akabekobeko/npm-icon-gen/issues/54) 144 | - ICNS displays incorrectly in Launchpad folder [#53](https://github.com/akabekobeko/npm-icon-gen/issues/53) 145 | 146 | ## 1.0.8 147 | 148 | ### Features 149 | 150 | - Drop transpile by Babel [#48](https://github.com/akabekobeko/npm-icon-gen/issues/48) 151 | 152 | ## v1.0.7 153 | 154 | ### Features 155 | 156 | - Update uuid to version 3.0.0 [#45](https://github.com/akabekobeko/npm-icon-gen/pull/45) by [marcbachmann (Marc Bachmann)](https://github.com/marcbachmann) 157 | 158 | ## 1.0.6 159 | 160 | ### Features 161 | 162 | - Node v7 support [#41](https://github.com/akabekobeko/npm-icon-gen/issues/41) 163 | 164 | ### Bug Fixes 165 | 166 | - Icns not working [#42](https://github.com/akabekobeko/npm-icon-gen/issues/42) 167 | - Fix icns generation [#43](https://github.com/akabekobeko/npm-icon-gen/pull/43) by [mifi (Mikael Finstad)](https://github.com/mifi) 168 | 169 | ## 1.0.5 170 | 171 | ### Features 172 | 173 | - Correct default for `options.type` [#39](https://github.com/akabekobeko/npm-icon-gen/pull/39) by [atdrago (Adam Drago)](https://github.com/atdrago) 174 | 175 | ## 1.0.4 176 | 177 | ### Features 178 | 179 | - Allow specifying icon file name [#38](https://github.com/akabekobeko/npm-icon-gen/issues/38) 180 | 181 | ## v1.0.3 182 | 183 | ### Features 184 | 185 | - ICNS size adjustments [#32](https://github.com/akabekobeko/npm-icon-gen/issues/32) 186 | 187 | ### Bug Fixes 188 | 189 | - It is an error to omit the modes in options [#33](https://github.com/akabekobeko/npm-icon-gen/issues/33) 190 | 191 | ## v1.0.2 192 | 193 | ### Features 194 | 195 | - Update a node modules 196 | - Node v6 support 197 | 198 | ## v1.0.1 199 | 200 | ### Features 201 | 202 | - Update a node modules 203 | - All of the file is output in a mode other than all [#25](https://github.com/akabekobeko/npm-icon-gen/issues/25) 204 | - Wrong PNG mode of message [#24](https://github.com/akabekobeko/npm-icon-gen/issues/24) 205 | - Implement an output mode [#22](https://github.com/akabekobeko/npm-icon-gen/issues/22) 206 | 207 | ## v1.0.0 208 | 209 | - First release 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 akabeko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-icon-gen 2 | 3 | [![Support Node of LTS](https://img.shields.io/badge/node-LTS-brightgreen.svg)](https://nodejs.org/) 4 | [![npm version](https://badge.fury.io/js/icon-gen.svg)](https://badge.fury.io/js/icon-gen) 5 | ![test](https://github.com/akabekobeko/npm-icon-gen/workflows/test/badge.svg) 6 | 7 | Generate an icon files from the **SVG** or **PNG** files. 8 | 9 | ## Support formats 10 | 11 | Supported the output format of the icon are following. 12 | 13 | | Platform | Icon | 14 | | -------: | ----------------------------------- | 15 | | Windows | `app.ico` or specified name. | 16 | | macOS | `app.icns` or specified name. | 17 | | Favicon | `favicon.ico` and `favicon-XX.png`. | 18 | 19 | ## Installation 20 | 21 | ``` 22 | $ npm install icon-gen 23 | ``` 24 | 25 | ## Usage 26 | 27 | SVG and PNG are automatically selected from the `input` path. If the path indicates a file **SVG**, if it is a directory it will be a **PNG** folder. 28 | 29 | ### SVG 30 | 31 | SVG files are rendering to PNG file in [sharp](https://www.npmjs.com/package/sharp). Rendering files is output to a temporary directory of the each OS. 32 | 33 | ```js 34 | const icongen = require('icon-gen') 35 | 36 | icongen('./sample.svg', './icons', { report: true }) 37 | .then((results) => { 38 | console.log(results) 39 | }) 40 | .catch((err) => { 41 | console.error(err) 42 | }) 43 | ``` 44 | 45 | Stopped using [svg2png](https://www.npmjs.com/package/svg2png) because of its dependency on [phantomjs](https://www.npmjs.com/package/phantomjs), which is deprecated. 46 | 47 | The quality of PNG generated from SVG will change, so if you need the previous results, use `icon-gen v2.1.0` (old version). 48 | 49 | ``` 50 | $ npm install icon-gen@2.1.0 51 | ``` 52 | 53 | In the future, I may add SVG to PNG conversion by Chromium via [puppeteer-core](https://www.npmjs.com/package/puppeteer-core) in addition to [sharp](https://www.npmjs.com/package/sharp). 54 | 55 | ### PNG 56 | 57 | Generate an icon files from the directory of PNG files. 58 | 59 | ```js 60 | const icongen = require('icon-gen') 61 | 62 | icongen('./images', './icons', { report: true }) 63 | .then((results) => { 64 | console.log(results) 65 | }) 66 | .catch((err) => { 67 | console.error(err) 68 | }) 69 | ``` 70 | 71 | Required PNG files is below. Favicon outputs both the ICO and PNG files (see: [audreyr/favicon-cheat-sheet](https://github.com/audreyr/favicon-cheat-sheet)). 72 | 73 | | Name | Size | ICO | ICNS | Fav ICO | Fav PNG | 74 | | -------: | --------: | :------: | :------: | :------: | :------: | 75 | | 16.png | 16x16 | ✔ | ✔ | ✔ | | 76 | | 24.png | 24x24 | ✔ | | ✔ | | 77 | | 32.png | 32x32 | ✔ | ✔ | ✔ | ✔ | 78 | | 48.png | 48x48 | ✔ | | ✔ | | 79 | | 57.png | 57x57 | | | | ✔ | 80 | | 64.png | 64x64 | ✔ | ✔ | ✔ | | 81 | | 72.png | 72x72 | | | | ✔ | 82 | | 96.png | 96x96 | | | | ✔ | 83 | | 120.png | 120x120 | | | | ✔ | 84 | | 128.png | 128x128 | ✔ | ✔ | | ✔ | 85 | | 144.png | 144x144 | | | | ✔ | 86 | | 152.png | 152x152 | | | | ✔ | 87 | | 195.png | 195x195 | | | | ✔ | 88 | | 228.png | 228x228 | | | | ✔ | 89 | | 256.png | 256x256 | ✔ | ✔ | | | 90 | | 512.png | 512x512 | | ✔ | | | 91 | | 1024.png | 1024x1024 | | ✔ | | | 92 | 93 | To make it a special size configuration, please specify with `ico`,`icns` and `favicon` options. 94 | 95 | ## Node API 96 | 97 | ### icongen 98 | 99 | **icongen** is promisify function. 100 | 101 | `icongen(src, dest[, options])` 102 | 103 | | Name | Type | Description | 104 | | ------- | -------- | ---------------------------------------------------------------------------- | 105 | | src | `String` | Path of the **SVG file** or **PNG files directory** that becomes the source. | 106 | | dest | `String` | Destination directory path. | 107 | | options | `Object` | see: _Options_. | 108 | 109 | _Options:_ 110 | 111 | ```js 112 | const options = { 113 | report: true, 114 | ico: { 115 | name: 'app', 116 | sizes: [16, 24, 32, 48, 64, 128, 256] 117 | }, 118 | icns: { 119 | name: 'app', 120 | sizes: [16, 32, 64, 128, 256, 512, 1024] 121 | }, 122 | favicon: { 123 | name: 'favicon-', 124 | pngSizes: [32, 57, 72, 96, 120, 128, 144, 152, 195, 228], 125 | icoSizes: [16, 24, 32, 48, 64] 126 | } 127 | } 128 | ``` 129 | 130 | If all image options (`ico`,`icns`, `favicon`) are omitted, all images are output with their default settings. 131 | 132 | ```js 133 | // Output an all images with default settings 134 | const options = { 135 | report: true 136 | } 137 | ``` 138 | 139 | If individual image option is omitted, default setting is used. If there is a format that you do not want to output, specify others and omit that image. 140 | 141 | ```js 142 | // Without ICNS 143 | const options = { 144 | report: true, 145 | ico: {} 146 | favicon: {} 147 | } 148 | ``` 149 | 150 | | Name | Type | Description | 151 | | ------- | --------- | ------------------------------------------------------------------ | 152 | | report | `Boolean` | Display the process reports. Default is `false`, disable a report. | 153 | | ico | `Object` | Output setting of ICO file. | 154 | | icns | `Object` | Output setting of ICNS file. | 155 | | favicon | `Object` | Output setting of Favicon file (PNG and ICO). | 156 | 157 | _`ico`, `icns`_ 158 | 159 | | Name | Type | Default | Description | 160 | | ----- | ---------- | --------------- | ---------------------------- | 161 | | name | `String` | `app` | Name of an output file. | 162 | | sizes | `Number[]` | `[Defaults...]` | Structure of an image sizes. | 163 | 164 | _`favicon`_ 165 | 166 | | Name | Type | Default | Description | 167 | | -------- | ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | 168 | | name | `String` | `favicon-` | Prefix of an output PNG files. Start with the alphabet, can use `-` and `_`. This option is for PNG. The name of the ICO file is always `favicon.ico`. | 169 | | pngSizes | `Number[]` | `[Defaults...]` | Size structure of PNG files to output. | 170 | | icoSizes | `Number[]` | `[Defaults...]` | Structure of an image sizes for ICO. | 171 | 172 | ## CLI 173 | 174 | ``` 175 | Usage: icon-gen [options] 176 | 177 | Generate an icon from the SVG or PNG file. 178 | If "--ico", "--icns", "--favicon" is not specified, everything is output in the standard setting. 179 | 180 | Options: 181 | -i, --input Path of the SVG file or PNG file directory. 182 | -o, --output Path of the output directory. 183 | -r, --report Display the process reports, default is disable. 184 | --ico Output ICO file with default settings, option is "--ico-*". 185 | --ico-name ICO file name to output. 186 | --ico-sizes [Sizes] PNG size list to structure ICO file 187 | --icns Output ICNS file with default settings, option is "--icns-*". 188 | --icns-name ICO file name to output. 189 | --icns-sizes [Sizes] PNG size list to structure ICNS file 190 | --favicon Output Favicon files with default settings, option is "--favicon-*". 191 | --favicon-name prefix of the PNG file. Start with the alphabet, can use "-" and "_" 192 | --favicon-png-sizes [Sizes] Sizes of the Favicon PNG files 193 | --favicon-ico-sizes [Sizes] PNG size list to structure Favicon ICO file 194 | -v, --version output the version number 195 | -h, --help output usage information 196 | 197 | Examples: 198 | $ icon-gen -i sample.svg -o ./dist -r 199 | $ icon-gen -i ./images -o ./dist -r 200 | $ icon-gen -i sample.svg -o ./dist --ico --icns 201 | $ icon-gen -i sample.svg -o ./dist --ico --ico-name sample --ico-sizes 16,32 202 | $ icon-gen -i sample.svg -o ./dist --icns --icns-name sample --icns-sizes 16,32 203 | $ icon-gen -i sample.svg -o ./dist --favicon --favicon-name=favicon- --favicon-png-sizes 16,32,128 --favicon-ico-sizes 16,32 204 | 205 | See also: 206 | https://github.com/akabekobeko/npm-icon-gen 207 | ``` 208 | 209 | # ChangeLog 210 | 211 | - [CHANGELOG](CHANGELOG.md) 212 | 213 | # License 214 | 215 | - [MIT](LICENSE.txt) 216 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples for icon-gen 2 | 3 | ## Installation 4 | 5 | ``` 6 | $ npm install 7 | ``` 8 | 9 | ## Usage 10 | 11 | `js` is JavaScript. `ts` is TypeScript on `ts-node`. 12 | 13 | Generate from SVG. 14 | 15 | ``` 16 | $ npm run js:svg 17 | $ npm run ts:svg 18 | ``` 19 | 20 | Generate from PNG. 21 | 22 | ``` 23 | $ npm run js:png 24 | $ npm run ts:png 25 | ``` 26 | 27 | ## License 28 | 29 | MIT 30 | -------------------------------------------------------------------------------- /examples/clean.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const clean = (dir) => { 5 | const items = fs.readdirSync(dir) 6 | for (const item of items) { 7 | switch (path.extname(item)) { 8 | case '.ico': 9 | case '.icns': 10 | case '.png': 11 | fs.unlinkSync(path.join(dir, item)) 12 | break 13 | 14 | default: 15 | break 16 | } 17 | } 18 | } 19 | 20 | module.exports = clean 21 | -------------------------------------------------------------------------------- /examples/data/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/1024.png -------------------------------------------------------------------------------- /examples/data/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/120.png -------------------------------------------------------------------------------- /examples/data/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/128.png -------------------------------------------------------------------------------- /examples/data/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/144.png -------------------------------------------------------------------------------- /examples/data/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/152.png -------------------------------------------------------------------------------- /examples/data/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/16.png -------------------------------------------------------------------------------- /examples/data/195.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/195.png -------------------------------------------------------------------------------- /examples/data/228.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/228.png -------------------------------------------------------------------------------- /examples/data/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/24.png -------------------------------------------------------------------------------- /examples/data/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/256.png -------------------------------------------------------------------------------- /examples/data/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/32.png -------------------------------------------------------------------------------- /examples/data/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/48.png -------------------------------------------------------------------------------- /examples/data/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/512.png -------------------------------------------------------------------------------- /examples/data/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/57.png -------------------------------------------------------------------------------- /examples/data/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/64.png -------------------------------------------------------------------------------- /examples/data/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/72.png -------------------------------------------------------------------------------- /examples/data/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/data/96.png -------------------------------------------------------------------------------- /examples/data/sample.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/filename.js: -------------------------------------------------------------------------------- 1 | const icongen = require('icon-gen') 2 | 3 | const options = { 4 | ico: { 5 | name: 'foo' 6 | }, 7 | icns: { 8 | name: 'bar' 9 | }, 10 | favicon: { 11 | name: 'icon-' 12 | }, 13 | report: true 14 | } 15 | 16 | icongen('./data/sample.svg', './icons', options) 17 | .then((results) => { 18 | console.log('Completed!!') 19 | }) 20 | .catch((err) => { 21 | console.error(err) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/from-png.js: -------------------------------------------------------------------------------- 1 | const icongen = require('icon-gen') 2 | require('./clean')('./icons') 3 | 4 | const options = { 5 | report: true 6 | } 7 | 8 | icongen('./data', './icons', options) 9 | .then((results) => { 10 | console.log(results) 11 | console.log('Completed!!') 12 | }) 13 | .catch((err) => { 14 | console.error(err) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/from-png.ts: -------------------------------------------------------------------------------- 1 | import icongen from 'icon-gen' 2 | require('./clean')('./icons') 3 | 4 | const options = { 5 | report: true 6 | } 7 | 8 | icongen('./data', './icons', options) 9 | .then((results) => { 10 | console.log(results) 11 | console.log('Completed!!') 12 | }) 13 | .catch((err) => { 14 | console.error(err) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/from-svg.js: -------------------------------------------------------------------------------- 1 | const icongen = require('icon-gen') 2 | require('./clean')('./icons') 3 | 4 | const options = { 5 | report: true 6 | } 7 | 8 | icongen('./data/sample.svg', './icons', options) 9 | .then((results) => { 10 | console.log(results) 11 | console.log('Completed!!') 12 | }) 13 | .catch((err) => { 14 | console.error(err) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/from-svg.ts: -------------------------------------------------------------------------------- 1 | import icongen from 'icon-gen' 2 | require('./clean')('./icons') 3 | 4 | const options = { 5 | report: true 6 | } 7 | 8 | icongen('./data/sample.svg', './icons', options) 9 | .then((results) => { 10 | console.log(results) 11 | console.log('Completed!!') 12 | }) 13 | .catch((err) => { 14 | console.error(err) 15 | }) 16 | -------------------------------------------------------------------------------- /examples/icons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akabekobeko/npm-icon-gen/06e93c6572f19593ce810b8d3df4ee304446db35/examples/icons/.gitkeep -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icon-gen-examples", 3 | "description": "Examples for the icon-gen.", 4 | "private": true, 5 | "version": "1.0.0", 6 | "author": "akabeko (http://akabeko.me/)", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "scripts": { 10 | "js:svg": "node from-svg.js", 11 | "js:png": "node from-png.js", 12 | "ts:svg": "ts-node from-svg.ts", 13 | "ts:png": "ts-node from-png.ts" 14 | }, 15 | "dependencies": { 16 | "icon-gen": "^5.0.0", 17 | "ts-node": "^10.9.2", 18 | "typescript": "^5.5.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icon-gen", 3 | "description": "Generate an icon files from the SVG or PNG files", 4 | "version": "5.0.0", 5 | "author": "akabeko (http://akabeko.me/)", 6 | "license": "MIT", 7 | "homepage": "https://github.com/akabekobeko/npm-icon-gen#readme", 8 | "engines": { 9 | "node": ">= 20" 10 | }, 11 | "volta": { 12 | "node": "20.15.1" 13 | }, 14 | "main": "dist/lib/index.js", 15 | "bin": { 16 | "icon-gen": "dist/bin/index.js" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "keywords": [ 22 | "Icon", 23 | "Generator", 24 | "SVG", 25 | "CLI" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/akabekobeko/npm-icon-gen.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/akabekobeko/npm-icon-gen/issues" 33 | }, 34 | "scripts": { 35 | "test": "vitest", 36 | "start": "npm run watch", 37 | "tsc": "tsc --noEmit", 38 | "build": "tsc", 39 | "watch": "tsc -w", 40 | "prepare": "npm run build" 41 | }, 42 | "dependencies": { 43 | "commander": "^12.1.0", 44 | "pngjs": "^7.0.0", 45 | "sharp": "^0.33.4" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^20.14.10", 49 | "@types/pngjs": "^6.0.5", 50 | "typescript": "^5.5.3", 51 | "vitest": "^2.0.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/cli.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { parseArgv } from './cli' 3 | 4 | test('--input', () => { 5 | const argv = ['', '', '--input', 'sample.svg'] 6 | const options = parseArgv(argv) 7 | expect(options.input).toBe('sample.svg') 8 | }) 9 | 10 | test('-i', () => { 11 | const argv = ['', '', '-i', 'sample.svg'] 12 | const options = parseArgv(argv) 13 | expect(options.input).toBe('sample.svg') 14 | }) 15 | 16 | test('--output', () => { 17 | const argv = ['', '', '--output', './'] 18 | const options = parseArgv(argv) 19 | expect(options.output).toBe('./') 20 | }) 21 | 22 | test('-o', () => { 23 | const argv = ['', '', '-o', './'] 24 | const options = parseArgv(argv) 25 | expect(options.output).toBe('./') 26 | }) 27 | 28 | test('parseArgv: icon', () => { 29 | const argv = ['', '', '-i', 'sample.svg'] 30 | const options = parseArgv(argv) 31 | expect(options.icon).toStrictEqual({ 32 | report: false, 33 | icns: {}, 34 | ico: {}, 35 | favicon: {} 36 | }) 37 | }) 38 | 39 | test('--ico', () => { 40 | const argv = ['', '', '--ico'] 41 | const options = parseArgv(argv) 42 | expect(options.icon).toStrictEqual({ 43 | report: false, 44 | ico: {} 45 | }) 46 | }) 47 | 48 | test('--ico-name', () => { 49 | const argv = ['', '', '--ico', '--ico-name', 'sample'] 50 | const options = parseArgv(argv) 51 | expect(options.icon).toStrictEqual({ 52 | report: false, 53 | ico: { name: 'sample' } 54 | }) 55 | }) 56 | 57 | test('--ico-sizes', () => { 58 | const argv = ['', '', '--ico', '--ico-sizes', '24,32'] 59 | const options = parseArgv(argv) 60 | expect(options.icon).toStrictEqual({ 61 | report: false, 62 | ico: { sizes: [24, 32] } 63 | }) 64 | }) 65 | 66 | test('--icns', () => { 67 | const argv = ['', '', '--icns'] 68 | const options = parseArgv(argv) 69 | expect(options.icon).toStrictEqual({ 70 | report: false, 71 | icns: {} 72 | }) 73 | }) 74 | 75 | test('--icns-name', () => { 76 | const argv = ['', '', '--icns', '--icns-name', 'sample'] 77 | const options = parseArgv(argv) 78 | expect(options.icon).toStrictEqual({ 79 | report: false, 80 | icns: { name: 'sample' } 81 | }) 82 | }) 83 | 84 | test('--icns-sizes', () => { 85 | const argv = ['', '', '--icns', '--icns-sizes', '24,32'] 86 | const options = parseArgv(argv) 87 | expect(options.icon).toStrictEqual({ 88 | report: false, 89 | icns: { sizes: [24, 32] } 90 | }) 91 | }) 92 | 93 | test('--favicon', () => { 94 | const argv = ['', '', '--favicon'] 95 | const options = parseArgv(argv) 96 | expect(options.icon).toStrictEqual({ 97 | report: false, 98 | favicon: {} 99 | }) 100 | }) 101 | 102 | test('--favicon-name', () => { 103 | const argv = ['', '', '--favicon', '--favicon-name', 'sample'] 104 | const options = parseArgv(argv) 105 | expect(options.icon).toStrictEqual({ 106 | report: false, 107 | favicon: { name: 'sample' } 108 | }) 109 | }) 110 | 111 | test('--favicon-png-sizes', () => { 112 | const argv = ['', '', '--favicon', '--favicon-png-sizes', '24,32'] 113 | const options = parseArgv(argv) 114 | expect(options.icon).toStrictEqual({ 115 | report: false, 116 | favicon: { pngSizes: [24, 32] } 117 | }) 118 | }) 119 | 120 | test('--favicon-ico-sizes', () => { 121 | const argv = ['', '', '--favicon', '--favicon-ico-sizes', '24,32'] 122 | const options = parseArgv(argv) 123 | expect(options.icon).toStrictEqual({ 124 | report: false, 125 | favicon: { icoSizes: [24, 32] } 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander' 2 | import generateIcon, { ICONOptions } from '../lib/index' 3 | 4 | /** Options of command line interface. */ 5 | type CLIOptions = { 6 | /** Path of the SVG file or PNG file directory. */ 7 | input: string 8 | /** Path of the output directory. */ 9 | output: string 10 | /** Options of the icon generation. */ 11 | icon: ICONOptions 12 | } 13 | 14 | /** 15 | * Parse the part related to icon generation from options obtained by commander. 16 | * @param opts Options obtained by commander. 17 | * @returns Options of the icon generation. 18 | */ 19 | const parseIconOptions = (opts: any): ICONOptions => { 20 | const results: ICONOptions = { report: !!opts.report } 21 | if (opts.ico) { 22 | results.ico = {} 23 | if (opts.icoName) { 24 | results.ico.name = opts.icoName 25 | } 26 | 27 | if (opts.icoSizes) { 28 | results.ico.sizes = opts.icoSizes 29 | } 30 | } 31 | 32 | if (opts.icns) { 33 | results.icns = {} 34 | if (opts.icnsName) { 35 | results.icns.name = opts.icnsName 36 | } 37 | 38 | if (opts.icnsSizes) { 39 | results.icns.sizes = opts.icnsSizes 40 | } 41 | } 42 | 43 | if (opts.favicon) { 44 | results.favicon = {} 45 | if (opts.faviconName) { 46 | results.favicon.name = opts.faviconName 47 | } 48 | 49 | if (opts.faviconPngSizes) { 50 | results.favicon.pngSizes = opts.faviconPngSizes 51 | } 52 | 53 | if (opts.faviconIcoSizes) { 54 | results.favicon.icoSizes = opts.faviconIcoSizes 55 | } 56 | } 57 | 58 | // Generate all with default settings 59 | if (!(results.ico || results.icns || results.favicon)) { 60 | results.ico = {} 61 | results.icns = {} 62 | results.favicon = {} 63 | } 64 | 65 | return results 66 | } 67 | 68 | /** 69 | * Parse the sizes specification. 70 | * @param arg Argument of command line interface. 71 | * @returns Size of PNG images. 72 | */ 73 | const parseSizes = (arg: string): number[] => { 74 | return arg.split(',').map((n) => Number(n)) 75 | } 76 | 77 | /** 78 | * Parse the arguments of command line interface. 79 | * @param argv Arguments of command line interface. 80 | * @returns Parsed options. 81 | */ 82 | export const parseArgv = (argv: string[]): CLIOptions => { 83 | const program = new Command() 84 | program 85 | .usage('icon-gen [options]') 86 | .description( 87 | 'Generate an icon from the SVG or PNG file.\nIf "--ico", "--icns", "--favicon" is not specified, everything is output in the standard setting.' 88 | ) 89 | .option('-i, --input ', 'Path of the SVG file or PNG file directory.') 90 | .option('-o, --output ', 'Path of the output directory.') 91 | .option('-r, --report', 'Display the process reports, default is disable.') 92 | .option( 93 | '--ico', 94 | 'Output ICO file with default settings, option is "--ico-*".' 95 | ) 96 | .option('--ico-name ', 'ICO file name to output.') 97 | .option( 98 | '--ico-sizes [Sizes]', 99 | 'PNG size list to structure ICO file', 100 | parseSizes 101 | ) 102 | .option( 103 | '--icns', 104 | 'Output ICNS file with default settings, option is "--icns-*".' 105 | ) 106 | .option('--icns-name ', 'ICO file name to output.') 107 | .option( 108 | '--icns-sizes [Sizes]', 109 | 'PNG size list to structure ICNS file', 110 | parseSizes 111 | ) 112 | .option( 113 | '--favicon', 114 | 'Output Favicon files with default settings, option is "--favicon-*".' 115 | ) 116 | .option( 117 | '--favicon-name ', 118 | 'prefix of the PNG file. Start with the alphabet, can use "-" and "_"' 119 | ) 120 | .option( 121 | '--favicon-png-sizes [Sizes]', 122 | 'Sizes of the Favicon PNG files', 123 | parseSizes 124 | ) 125 | .option( 126 | '--favicon-ico-sizes [Sizes]', 127 | 'PNG size list to structure Favicon ICO file', 128 | parseSizes 129 | ) 130 | .version(require('../../package.json').version, '-v, --version') 131 | 132 | program.on('--help', () => { 133 | console.log(` 134 | Examples: 135 | $ icon-gen -i sample.svg -o ./dist -r 136 | $ icon-gen -i ./images -o ./dist -r 137 | $ icon-gen -i sample.svg -o ./dist --ico --icns 138 | $ icon-gen -i sample.svg -o ./dist --ico --ico-name sample --ico-sizes 16,32 139 | $ icon-gen -i sample.svg -o ./dist --icns --icns-name sample --icns-sizes 16,32 140 | $ icon-gen -i sample.svg -o ./dist --favicon --favicon-name=favicon- --favicon-png-sizes 16,32,128 --favicon-ico-sizes 16,32 141 | 142 | See also: 143 | https://github.com/akabekobeko/npm-icon-gen`) 144 | }) 145 | 146 | // Print help and exit if there are no arguments 147 | if (argv.length < 3) { 148 | program.help() 149 | } 150 | 151 | program.parse(argv) 152 | const opts = program.opts() 153 | return { 154 | input: opts.input, 155 | output: opts.output, 156 | icon: parseIconOptions(opts) 157 | } 158 | } 159 | 160 | /** 161 | * Run the tool based on command line arguments. 162 | * @param argv Arguments of command line interface. 163 | * @returns Path of generated files. 164 | */ 165 | const exec = (argv: string[]): Promise => { 166 | const options = parseArgv(argv) 167 | return generateIcon(options.input, options.output, options.icon) 168 | } 169 | 170 | export default exec 171 | -------------------------------------------------------------------------------- /src/bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import cli from './cli.js' 4 | 5 | cli(process.argv).catch((err) => { 6 | console.error(err) 7 | }) 8 | -------------------------------------------------------------------------------- /src/lib/favicon.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import path from 'node:path' 3 | import fs from 'node:fs' 4 | import Logger from './logger' 5 | import generateFavicon, { 6 | REQUIRED_IMAGE_SIZES, 7 | REQUIRED_PNG_SIZES, 8 | generatePNG 9 | } from './favicon' 10 | 11 | /** 12 | * Delete a files. 13 | * @param paths File paths. 14 | */ 15 | const deleteFiles = (paths: string[]) => { 16 | paths.forEach((path) => { 17 | try { 18 | const stat = fs.statSync(path) 19 | if (stat && stat.isFile()) { 20 | fs.unlinkSync(path) 21 | } 22 | } catch (err) { 23 | console.error(err) 24 | } 25 | }) 26 | } 27 | 28 | test('generateFavicon', () => { 29 | const images = REQUIRED_IMAGE_SIZES.map((size) => { 30 | const filePath = path.join('./examples/data', size + '.png') 31 | return { size, filePath } 32 | }) 33 | 34 | return generateFavicon(images, './examples/data', new Logger(), { 35 | name: '', 36 | pngSizes: [], 37 | icoSizes: [] 38 | }).then((results) => { 39 | expect(results.length).toBe(11) 40 | deleteFiles(results) 41 | }) 42 | }) 43 | 44 | test('generatePNG', () => { 45 | const images = REQUIRED_PNG_SIZES.map((size) => { 46 | const filePath = path.join('./examples/data', size + '.png') 47 | return { size, filePath } 48 | }) 49 | 50 | return generatePNG( 51 | images, 52 | './examples/data', 53 | 'favicon-', 54 | REQUIRED_PNG_SIZES, 55 | new Logger() 56 | ).then((results) => { 57 | expect(results.length).toBe(10) 58 | deleteFiles(results) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/lib/favicon.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import generateICO from './ico' 4 | import { ImageInfo, filterImagesBySizes } from './png' 5 | import Logger from './logger' 6 | 7 | /** Options ot generate ICO file. */ 8 | export type FavOptions = { 9 | /** Prefix of an output PNG files. Start with the alphabet, can use `-` and `_`. This option is for PNG. The name of the ICO file is always `favicon.ico`. */ 10 | name?: string 11 | /** Size structure of PNG files to output. */ 12 | pngSizes?: number[] 13 | /** Structure of an image sizes for ICO. */ 14 | icoSizes?: number[] 15 | } 16 | 17 | /** Sizes required for the PNG files. */ 18 | export const REQUIRED_PNG_SIZES = [32, 57, 72, 96, 120, 128, 144, 152, 195, 228] 19 | 20 | /** Sizes required for ICO file. */ 21 | export const REQUIRED_ICO_SIZES = [16, 24, 32, 48, 64] 22 | 23 | /** Sizes required for Favicon files. */ 24 | export const REQUIRED_IMAGE_SIZES = REQUIRED_PNG_SIZES.concat( 25 | REQUIRED_ICO_SIZES 26 | ) 27 | .filter((a, i, self) => self.indexOf(a) === i) 28 | .sort((a, b) => a - b) 29 | 30 | /** File name of Favicon file. */ 31 | const ICO_FILE_NAME = 'favicon' 32 | 33 | /** Prefix of PNG file names. */ 34 | const PNG_FILE_NAME_PREFIX = 'favicon-' 35 | 36 | /** 37 | * Copy to image. 38 | * @param image Image information. 39 | * @param dir Output destination The path of directory. 40 | * @param prefix Prefix of an output PNG files. Start with the alphabet, can use `-` and `_`. This option is for PNG. The name of the ICO file is always `favicon.ico`. 41 | * @param logger Logger. 42 | * @return Path of generated PNG file. 43 | */ 44 | const copyImage = ( 45 | image: ImageInfo, 46 | dir: string, 47 | prefix: string, 48 | logger: Logger 49 | ): Promise => { 50 | return new Promise((resolve, reject) => { 51 | const reader = fs.createReadStream(image.filePath).on('error', (err) => { 52 | reject(err) 53 | }) 54 | 55 | const dest = path.join(dir, `${prefix}${image.size}.png`) 56 | const writer = fs 57 | .createWriteStream(dest) 58 | .on('error', (err) => { 59 | reject(err) 60 | }) 61 | .on('close', () => { 62 | logger.log(' Create: ' + dest) 63 | resolve(dest) 64 | }) 65 | 66 | reader.pipe(writer) 67 | }) 68 | } 69 | 70 | /** 71 | * Generate the FAVICON PNG file from the PNG images. 72 | * @param images File information for the PNG files generation. 73 | * @param dir Output destination the path of directory. 74 | * @param prefix Prefix of an output PNG files. Start with the alphabet, can use `-` and `_`. This option is for PNG. The name of the ICO file is always `favicon.ico`. 75 | * @param sizes Size structure of PNG files to output. 76 | * @param logger Logger. 77 | * @return Path of the generated files. 78 | */ 79 | export const generatePNG = async ( 80 | images: ImageInfo[], 81 | dir: string, 82 | prefix: string, 83 | sizes: number[], 84 | logger: Logger 85 | ): Promise => { 86 | logger.log('Favicon:') 87 | 88 | const targets = filterImagesBySizes(images, sizes) 89 | const results = [] 90 | for (const image of targets) { 91 | results.push(await copyImage(image, dir, prefix, logger)) 92 | } 93 | 94 | return results 95 | } 96 | 97 | /** 98 | * Generate a FAVICON image files (ICO and PNG) from the PNG images. 99 | * @param images File information for the PNG files generation. 100 | * @param dir Output destination the path of directory. 101 | * @param logger Logger. 102 | * @param options Options. 103 | * @return Path of the generated files. 104 | */ 105 | const generateFavicon = async ( 106 | images: ImageInfo[], 107 | dir: string, 108 | logger: Logger, 109 | options: FavOptions 110 | ): Promise => { 111 | const opt = { 112 | name: 113 | options.name && options.name !== '' ? options.name : PNG_FILE_NAME_PREFIX, 114 | pngSizes: 115 | options.pngSizes && 0 < options.pngSizes.length 116 | ? options.pngSizes 117 | : REQUIRED_PNG_SIZES, 118 | icoSizes: 119 | options.icoSizes && 0 < options.icoSizes.length 120 | ? options.icoSizes 121 | : REQUIRED_ICO_SIZES 122 | } 123 | 124 | const results = await generatePNG(images, dir, opt.name, opt.pngSizes, logger) 125 | results.push( 126 | await generateICO(filterImagesBySizes(images, opt.icoSizes), dir, logger, { 127 | name: ICO_FILE_NAME, 128 | sizes: opt.icoSizes 129 | }) 130 | ) 131 | return results 132 | } 133 | 134 | export default generateFavicon 135 | -------------------------------------------------------------------------------- /src/lib/icns.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import Logger from './logger' 5 | import generateICNS, { REQUIRED_IMAGE_SIZES } from './icns' 6 | 7 | test('generateICNS', () => { 8 | const images = REQUIRED_IMAGE_SIZES.map((size) => { 9 | const filePath = path.join('./examples/data', size + '.png') 10 | return { size, filePath } 11 | }) 12 | 13 | return generateICNS(images, './examples/data', new Logger(), { 14 | name: '', 15 | sizes: [] 16 | }).then((filePath) => { 17 | // output file size must be at least larger than input file size 18 | expect( 19 | fs.statSync(filePath).size > 20 | fs.statSync(images[images.length - 1].filePath).size 21 | ).toBe(true) 22 | fs.unlinkSync(filePath) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/lib/icns.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import util from 'util' 4 | import { PNG } from 'pngjs' 5 | import { packICNS } from './rle' 6 | import { ImageInfo, filterImagesBySizes } from './png' 7 | import Logger from './logger' 8 | 9 | const readFileAsync = util.promisify(fs.readFile) 10 | const writeFileASync = util.promisify(fs.writeFile) 11 | 12 | /** Information of pack bit. */ 13 | type PackBitBody = { 14 | /** Colors of compressed by ICNS RLE. */ 15 | colors: number[] 16 | /** Masks of alpha color. */ 17 | masks: number[] 18 | } 19 | 20 | /** Icon information in ICNS. */ 21 | type IconInfo = { 22 | type: string 23 | size: number 24 | mask?: string 25 | } 26 | 27 | /** Options of ICNS. */ 28 | export type ICNSOptions = { 29 | /** Name of an output file. */ 30 | name?: string 31 | /** Structure of an image sizes. */ 32 | sizes?: number[] 33 | } 34 | 35 | /** 36 | * Sizes required for the ICNS file. 37 | * @type {Array} 38 | */ 39 | export const REQUIRED_IMAGE_SIZES = [16, 32, 64, 128, 256, 512, 1024] 40 | 41 | /** 42 | * The size of the ICNS header. 43 | * @type {Number} 44 | */ 45 | const HEADER_SIZE = 8 46 | 47 | /** 48 | * Identifier of the ICNS file, in ASCII "icns". 49 | * @type {Number} 50 | */ 51 | const FILE_HEADER_ID = 'icns' 52 | 53 | /** 54 | * Default file name. 55 | * @type {String} 56 | */ 57 | const DEFAULT_FILE_NAME = 'app' 58 | 59 | /** 60 | * ICNS file extension. 61 | * @type {String} 62 | */ 63 | const FILE_EXTENSION = '.icns' 64 | 65 | /** 66 | * Information of the images, Mac OS 8.x (il32, is32, l8mk, s8mk) is unsupported. 67 | * If icp4, icp5, icp6 is present, Icon will not be supported because it can not be set as Folder of Finder. 68 | */ 69 | const ICON_INFOS: IconInfo[] = [ 70 | // Normal 71 | { type: 'ic07', size: 128 }, 72 | { type: 'ic08', size: 256 }, 73 | { type: 'ic09', size: 512 }, 74 | { type: 'ic10', size: 1024 }, 75 | 76 | // Retina 77 | { type: 'ic11', size: 32 }, 78 | { type: 'ic12', size: 64 }, 79 | { type: 'ic13', size: 256 }, 80 | { type: 'ic14', size: 512 }, 81 | 82 | // Mac OS 8.5 83 | { type: 'is32', mask: 's8mk', size: 16 }, 84 | { type: 'il32', mask: 'l8mk', size: 32 } 85 | ] 86 | 87 | /** 88 | * Select the support image from the icon size. 89 | * @param size Size of icon. 90 | * @param images File information. 91 | * @return If successful image information, otherwise null. 92 | */ 93 | const imageFromIconSize = ( 94 | size: number, 95 | images: ImageInfo[] 96 | ): ImageInfo | null => { 97 | for (const image of images) { 98 | if (image.size === size) { 99 | return image 100 | } 101 | } 102 | 103 | return null 104 | } 105 | 106 | /** 107 | * Create the ICNS file header. 108 | * @param fileSize File size. 109 | * @return Header data. 110 | */ 111 | const createFileHeader = (fileSize: number): Buffer => { 112 | const buffer = Buffer.alloc(HEADER_SIZE) 113 | buffer.write(FILE_HEADER_ID, 0, 'ascii') 114 | buffer.writeUInt32BE(fileSize, 4) 115 | 116 | return buffer 117 | } 118 | 119 | /** 120 | * Create the Icon header in ICNS file. 121 | * @param type Type of the icon. 122 | * @param imageSize Size of the image data. 123 | * @return Header data. 124 | */ 125 | const createIconHeader = (type: string, imageSize: number): Buffer => { 126 | const buffer = Buffer.alloc(HEADER_SIZE) 127 | buffer.write(type, 0, 'ascii') 128 | buffer.writeUInt32BE(HEADER_SIZE + imageSize, 4) 129 | 130 | return buffer 131 | } 132 | 133 | /** 134 | * Create a color and mask data. 135 | * @param image Binary of image file. 136 | * @return Pack bit bodies. 137 | */ 138 | const createIconBlockPackBitsBodies = (image: Buffer): PackBitBody => { 139 | const png = PNG.sync.read(image) 140 | const results: PackBitBody = { colors: [], masks: [] } 141 | const r = [] 142 | const g = [] 143 | const b = [] 144 | 145 | for (let i = 0, max = png.data.length; i < max; i += 4) { 146 | // RGB 147 | r.push(png.data.readUInt8(i)) 148 | g.push(png.data.readUInt8(i + 1)) 149 | b.push(png.data.readUInt8(i + 2)) 150 | 151 | // Alpha 152 | results.masks.push(png.data.readUInt8(i + 3)) 153 | } 154 | 155 | // Compress 156 | results.colors = results.colors.concat(packICNS(r)) 157 | results.colors = results.colors.concat(packICNS(g)) 158 | results.colors = results.colors.concat(packICNS(b)) 159 | 160 | return results 161 | } 162 | 163 | /** 164 | * Create an icon block's data. 165 | * @param type Type of the icon. 166 | * @param image Binary of image file. 167 | * @return Binary of icon block. 168 | */ 169 | const createIconBlockData = (type: string, image: Buffer): Buffer => { 170 | const header = createIconHeader(type, image.length) 171 | return Buffer.concat([header, image], header.length + image.length) 172 | } 173 | 174 | /** 175 | * Create an icon blocks (Color and mask) for PackBits. 176 | * @param type Type of the icon in color block. 177 | * @param mask Type of the icon in mask block. 178 | * @param image Binary of image file. 179 | * @return Binary of icon block. 180 | */ 181 | const createIconBlockPackBits = ( 182 | type: string, 183 | mask: string, 184 | image: Buffer 185 | ): Buffer => { 186 | const bodies = createIconBlockPackBitsBodies(image) 187 | const colorBlock = createIconBlockData(type, Buffer.from(bodies.colors)) 188 | const maskBlock = createIconBlockData(mask, Buffer.from(bodies.masks)) 189 | 190 | return Buffer.concat( 191 | [colorBlock, maskBlock], 192 | colorBlock.length + maskBlock.length 193 | ) 194 | } 195 | 196 | /** 197 | * Create an icon block. 198 | * @param info Icon information in ICNS. 199 | * @param filePath Path of image (PNG) file. 200 | * @return Binary of icon block. 201 | */ 202 | const createIconBlock = async ( 203 | info: IconInfo, 204 | filePath: string 205 | ): Promise => { 206 | const image = await readFileAsync(filePath) 207 | 208 | switch (info.type) { 209 | case 'is32': 210 | case 'il32': 211 | return createIconBlockPackBits(info.type, info.mask || '', image) 212 | 213 | default: 214 | return createIconBlockData(info.type, image) 215 | } 216 | } 217 | 218 | /** 219 | * Create the ICNS file body on memory buffer. 220 | * @param images Information of the image files. 221 | * @returns Body of ICNS file. 222 | */ 223 | const createFileBody = async (images: ImageInfo[]): Promise => { 224 | let body = Buffer.alloc(0) 225 | for (const info of ICON_INFOS) { 226 | const image = imageFromIconSize(info.size, images) 227 | if (!image) { 228 | // Depending on the command line option, there may be no corresponding size 229 | continue 230 | } 231 | 232 | const block = await createIconBlock(info, image.filePath) 233 | body = Buffer.concat([body, block], body.length + block.length) 234 | } 235 | 236 | return body 237 | } 238 | 239 | /** 240 | * Create an ICNS file. 241 | * @param images Information of the image files. 242 | * @param filePath The path of the output destination file. 243 | * @return Asynchronous task. 244 | */ 245 | const createIconFile = async ( 246 | images: ImageInfo[], 247 | filePath: string 248 | ): Promise => { 249 | // Write images on memory buffer 250 | const body = await createFileBody(images) 251 | if (body.length === 0) { 252 | throw new Error('Failed to create the body of the file. The size is `0`.') 253 | } 254 | 255 | // Write file header and body 256 | return new Promise((resolve, reject) => { 257 | const stream = fs.createWriteStream(filePath) 258 | // https://stackoverflow.com/questions/12906694/fs-createwritestream-does-not-immediately-create-file 259 | stream.on('ready', () => { 260 | stream.write(createFileHeader(body.length + HEADER_SIZE), 'binary') 261 | stream.write(body, 'binary') 262 | stream.end() 263 | }) 264 | 265 | stream.on('error', (err) => reject(err)) 266 | 267 | // https://stackoverflow.com/questions/46752428/do-i-need-await-fs-createwritestream-in-pipe-method-in-node 268 | stream.on('finish', () => resolve()) 269 | }) 270 | } 271 | 272 | /** 273 | * Unpack an icon block files from ICNS file (For debug). 274 | * @param src Path of the ICNS file. 275 | * @param dest Path of directory to output icon block files. 276 | * @return Asynchronous task. 277 | */ 278 | export const debugUnpackIconBlocks = async ( 279 | src: string, 280 | dest: string 281 | ): Promise => { 282 | const data = await readFileAsync(src) 283 | for (let pos = HEADER_SIZE, max = data.length; pos < max; ) { 284 | const header = data.slice(pos, pos + HEADER_SIZE) 285 | const type = header.toString('ascii', 0, 4) 286 | const size = header.readUInt32BE(4) - HEADER_SIZE 287 | 288 | pos += HEADER_SIZE 289 | const body = data.slice(pos, pos + size) 290 | await writeFileASync(path.join(dest, `${type}.header`), header, 'binary') 291 | await writeFileASync(path.join(dest, `${type}.body`), body, 'binary') 292 | 293 | pos += size 294 | } 295 | } 296 | 297 | /** 298 | * Create the ICNS file from a PNG images. 299 | * @param images Information of the image files. 300 | * @param dir Output destination the path of directory. 301 | * @param logger Logger. 302 | * @param options Options. 303 | * @return Path of generated ICNS file. 304 | */ 305 | const generateICNS = async ( 306 | images: ImageInfo[], 307 | dir: string, 308 | logger: Logger, 309 | options: ICNSOptions 310 | ): Promise => { 311 | logger.log('ICNS:') 312 | 313 | const opt = { 314 | name: 315 | options.name && options.name !== '' ? options.name : DEFAULT_FILE_NAME, 316 | sizes: 317 | options.sizes && 0 < options.sizes.length 318 | ? options.sizes 319 | : REQUIRED_IMAGE_SIZES 320 | } 321 | 322 | const dest = path.join(dir, opt.name + FILE_EXTENSION) 323 | try { 324 | const targets = filterImagesBySizes(images, opt.sizes) 325 | await createIconFile(targets, dest) 326 | } catch (err) { 327 | fs.unlinkSync(dest) 328 | throw err 329 | } 330 | 331 | logger.log(' Create: ' + dest) 332 | return dest 333 | } 334 | 335 | export default generateICNS 336 | -------------------------------------------------------------------------------- /src/lib/ico.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import Logger from './logger' 5 | import generateICO, { REQUIRED_IMAGE_SIZES } from './ico' 6 | 7 | test('generateICO', () => { 8 | const targets = REQUIRED_IMAGE_SIZES.map((size) => { 9 | const filePath = path.join('./examples/data', size + '.png') 10 | return { size, filePath } 11 | }) 12 | 13 | generateICO(targets, './examples/data', new Logger(), {}).then((result) => { 14 | // output file size must be at least larger than input file size 15 | expect( 16 | fs.statSync(result).size > 17 | fs.statSync(targets[targets.length - 1].filePath).size 18 | ).toBe(true) 19 | fs.unlinkSync(result) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/lib/ico.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { PNG } from 'pngjs' 4 | import { ImageInfo, filterImagesBySizes } from './png' 5 | import Logger from './logger' 6 | 7 | /** Options of `generateICO`. */ 8 | export type ICOOptions = { 9 | /** Name of an output file. */ 10 | name?: string 11 | /** Structure of an image sizes. */ 12 | sizes?: number[] 13 | } 14 | 15 | /** Sizes required for the ICO file. */ 16 | export const REQUIRED_IMAGE_SIZES = [16, 24, 32, 48, 64, 128, 256] 17 | 18 | /** Default name of ICO file. */ 19 | const DEFAULT_FILE_NAME = 'app' 20 | 21 | /** File extension of ICO file. */ 22 | const FILE_EXTENSION = '.ico' 23 | 24 | /** Size of the file header. */ 25 | const FILE_HEADER_SIZE = 6 26 | 27 | /** Size of the icon directory. */ 28 | const ICO_DIRECTORY_SIZE = 16 29 | 30 | /** Size of the `BITMAPINFOHEADER`. */ 31 | const BITMAPINFOHEADER_SIZE = 40 32 | 33 | /** Color mode of `BITMAPINFOHEADER`.*/ 34 | const BI_RGB = 0 35 | 36 | /** BPP (Bit Per Pixel) for Alpha PNG (RGB = 4). */ 37 | const BPP_ALPHA = 4 38 | 39 | /** 40 | * Convert a PNG of the byte array to the DIB (Device Independent Bitmap) format. 41 | * PNG in color RGBA (and more), the coordinate structure is the Top/Left to Bottom/Right. 42 | * DIB in color BGRA, the coordinate structure is the Bottom/Left to Top/Right. 43 | * @param src Target image. 44 | * @param width The width of the image. 45 | * @param height The height of the image. 46 | * @param bpp The bit per pixel of the image. 47 | * @return Converted image 48 | * @see https://en.wikipedia.org/wiki/BMP_file_format 49 | */ 50 | const convertPNGtoDIB = ( 51 | src: Buffer, 52 | width: number, 53 | height: number, 54 | bpp: number 55 | ) => { 56 | const cols = width * bpp 57 | const rows = height * cols 58 | const rowEnd = rows - cols 59 | const dest = Buffer.alloc(src.length) 60 | 61 | for (let row = 0; row < rows; row += cols) { 62 | for (let col = 0; col < cols; col += bpp) { 63 | // RGBA: Top/Left -> Bottom/Right 64 | let pos = row + col 65 | const r = src.readUInt8(pos) 66 | const g = src.readUInt8(pos + 1) 67 | const b = src.readUInt8(pos + 2) 68 | const a = src.readUInt8(pos + 3) 69 | 70 | // BGRA: Right/Left -> Top/Right 71 | pos = rowEnd - row + col 72 | dest.writeUInt8(b, pos) 73 | dest.writeUInt8(g, pos + 1) 74 | dest.writeUInt8(r, pos + 2) 75 | dest.writeUInt8(a, pos + 3) 76 | } 77 | } 78 | 79 | return dest 80 | } 81 | 82 | /** 83 | * Create the `BITMAPINFOHEADER`. 84 | * @param png PNG image. 85 | * @param compression Compression mode 86 | * @return `BITMAPINFOHEADER` data. 87 | * @see https://msdn.microsoft.com/ja-jp/library/windows/desktop/dd183376%28v=vs.85%29.aspx 88 | */ 89 | const createBitmapInfoHeader = (png: PNG, compression: number) => { 90 | const b = Buffer.alloc(BITMAPINFOHEADER_SIZE) 91 | b.writeUInt32LE(BITMAPINFOHEADER_SIZE, 0) // 4 DWORD biSize 92 | b.writeInt32LE(png.width, 4) // 4 LONG biWidth 93 | b.writeInt32LE(png.height * 2, 8) // 4 LONG biHeight 94 | b.writeUInt16LE(1, 12) // 2 WORD biPlanes 95 | b.writeUInt16LE(BPP_ALPHA * 8, 14) // 2 WORD biBitCount 96 | b.writeUInt32LE(compression, 16) // 4 DWORD biCompression 97 | b.writeUInt32LE(png.data.length, 20) // 4 DWORD biSizeImage 98 | b.writeInt32LE(0, 24) // 4 LONG biXPelsPerMeter 99 | b.writeInt32LE(0, 28) // 4 LONG biYPelsPerMeter 100 | b.writeUInt32LE(0, 32) // 4 DWORD biClrUsed 101 | b.writeUInt32LE(0, 36) // 4 DWORD biClrImportant 102 | 103 | return b 104 | } 105 | 106 | /** 107 | * Create the Icon entry. 108 | * @param png PNG image. 109 | * @param offset The offset of directory data from the beginning of the ICO/CUR file 110 | * @return Directory data. 111 | * 112 | * @see https://msdn.microsoft.com/en-us/library/ms997538.aspx 113 | */ 114 | const createDirectory = (png: PNG, offset: number) => { 115 | const b = Buffer.alloc(ICO_DIRECTORY_SIZE) 116 | const size = png.data.length + BITMAPINFOHEADER_SIZE 117 | const width = 256 <= png.width ? 0 : png.width 118 | const height = 256 <= png.height ? 0 : png.height 119 | const bpp = BPP_ALPHA * 8 120 | 121 | b.writeUInt8(width, 0) // 1 BYTE Image width 122 | b.writeUInt8(height, 1) // 1 BYTE Image height 123 | b.writeUInt8(0, 2) // 1 BYTE Colors 124 | b.writeUInt8(0, 3) // 1 BYTE Reserved 125 | b.writeUInt16LE(1, 4) // 2 WORD Color planes 126 | b.writeUInt16LE(bpp, 6) // 2 WORD Bit per pixel 127 | b.writeUInt32LE(size, 8) // 4 DWORD Bitmap (DIB) size 128 | b.writeUInt32LE(offset, 12) // 4 DWORD Offset 129 | 130 | return b 131 | } 132 | 133 | /** 134 | * Create the ICO file header. 135 | * @param count Specifies number of images in the file. 136 | * @return Header data. 137 | * @see https://msdn.microsoft.com/en-us/library/ms997538.aspx 138 | */ 139 | const createFileHeader = (count: number) => { 140 | const b = Buffer.alloc(FILE_HEADER_SIZE) 141 | b.writeUInt16LE(0, 0) // 2 WORD Reserved 142 | b.writeUInt16LE(1, 2) // 2 WORD Type 143 | b.writeUInt16LE(count, 4) // 2 WORD Image count 144 | 145 | return b 146 | } 147 | 148 | /** 149 | * Read PNG data from image files. 150 | * @param images Information of image files. 151 | * @param sizes Target size of image. 152 | * @returns PNG data. 153 | */ 154 | const readPNGs = (images: ImageInfo[], sizes: number[]): PNG[] => { 155 | const targets = filterImagesBySizes(images, sizes) 156 | return targets.map((image) => { 157 | const data = fs.readFileSync(image.filePath) 158 | return PNG.sync.read(data) 159 | }) 160 | } 161 | 162 | /** 163 | * Write ICO directory information to the stream. 164 | * @param pngs PNG data. 165 | * @param stream Stream to write. 166 | */ 167 | const writeDirectories = (pngs: PNG[], stream: fs.WriteStream) => { 168 | let offset = FILE_HEADER_SIZE + ICO_DIRECTORY_SIZE * pngs.length 169 | for (const png of pngs) { 170 | const directory = createDirectory(png, offset) 171 | stream.write(directory, 'binary') 172 | offset += png.data.length + BITMAPINFOHEADER_SIZE 173 | } 174 | } 175 | 176 | /** 177 | * Write PNG data to the stream. 178 | * @param pngs PNG data. 179 | * @param stream Stream to write. 180 | */ 181 | const writePNGs = (pngs: PNG[], stream: fs.WriteStream) => { 182 | for (const png of pngs) { 183 | const header = createBitmapInfoHeader(png, BI_RGB) 184 | stream.write(header, 'binary') 185 | 186 | const dib = convertPNGtoDIB(png.data, png.width, png.height, BPP_ALPHA) 187 | stream.write(dib, 'binary') 188 | } 189 | } 190 | 191 | /** 192 | * Create an ICO file. 193 | * @param pngs Information of PNG images. 194 | * @param filePath The path of the output destination file. 195 | * @return Asynchronous task. 196 | */ 197 | const createIconFile = (pngs: PNG[], filePath: string): Promise => { 198 | return new Promise((resolve, reject) => { 199 | if (pngs.length === 0) { 200 | return reject( 201 | new Error('There was no PNG file matching the specified size.') 202 | ) 203 | } 204 | 205 | const stream = fs.createWriteStream(filePath) 206 | // https://stackoverflow.com/questions/12906694/fs-createwritestream-does-not-immediately-create-file 207 | stream.on('ready', () => { 208 | stream.write(createFileHeader(pngs.length), 'binary') 209 | writeDirectories(pngs, stream) 210 | writePNGs(pngs, stream) 211 | stream.end() 212 | }) 213 | 214 | stream.on('error', (err) => reject(err)) 215 | 216 | // https://stackoverflow.com/questions/46752428/do-i-need-await-fs-createwritestream-in-pipe-method-in-node 217 | stream.on('finish', () => resolve()) 218 | }) 219 | } 220 | 221 | /** 222 | * Generate the ICO file from a PNG images. 223 | * @param images File information. 224 | * @param dir Output destination the path of directory. 225 | * @param logger Logger. 226 | * @param options Options. 227 | * @return Path of the generated ICO file. 228 | */ 229 | const generateICO = async ( 230 | images: ImageInfo[], 231 | dir: string, 232 | logger: Logger, 233 | options: ICOOptions 234 | ): Promise => { 235 | logger.log('ICO:') 236 | 237 | const opt = { 238 | name: 239 | options.name && options.name !== '' ? options.name : DEFAULT_FILE_NAME, 240 | sizes: 241 | options.sizes && 0 < options.sizes.length 242 | ? options.sizes 243 | : REQUIRED_IMAGE_SIZES 244 | } 245 | 246 | const dest = path.join(dir, opt.name + FILE_EXTENSION) 247 | try { 248 | const pngs = readPNGs(images, opt.sizes) 249 | await createIconFile(pngs, dest) 250 | } catch (err) { 251 | fs.unlinkSync(dest) 252 | throw err 253 | } 254 | 255 | logger.log(' Create: ' + dest) 256 | return dest 257 | } 258 | 259 | export default generateICO 260 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import os from 'node:os' 4 | import generatePNG, { ImageInfo } from './png' 5 | import generateICO, { REQUIRED_IMAGE_SIZES as ICO_SIZES } from './ico' 6 | import generateICNS, { REQUIRED_IMAGE_SIZES as ICNS_SIZES } from './icns' 7 | import generateFavicon, { REQUIRED_IMAGE_SIZES as FAV_SIZES } from './favicon' 8 | import Logger from './logger' 9 | 10 | /** Options of icon generation. */ 11 | export type ICONOptions = { 12 | /** Output setting of ICO file. */ 13 | ico?: { 14 | /** Name of an output file. */ 15 | name?: string 16 | /** Structure of an image sizes. */ 17 | sizes?: number[] 18 | } 19 | 20 | /** Output setting of ICNS file. */ 21 | icns?: { 22 | /** Name of an output file. */ 23 | name?: string 24 | /** Structure of an image sizes. */ 25 | sizes?: number[] 26 | } 27 | 28 | /** Output setting of Favicon file (PNG and ICO). */ 29 | favicon?: { 30 | /** Prefix of an output PNG files. Start with the alphabet, can use `-` and `_`. This option is for PNG. The name of the ICO file is always `favicon.ico`. */ 31 | name?: string 32 | /** Size structure of PNG files to output. */ 33 | pngSizes?: number[] 34 | /** Structure of an image sizes for ICO. */ 35 | icoSizes?: number[] 36 | } 37 | 38 | /** `true` to display the processing status of the tool to `stdout`. */ 39 | report: boolean 40 | } 41 | 42 | /** 43 | * Filter the sizes. 44 | * @param sizes Original sizes. 45 | * @param filterSizes Filter sizes. 46 | * @return filtered sizes. 47 | */ 48 | const filterSizes = (sizes: number[] = [], filterSizes: number[] = []) => { 49 | if (filterSizes.length === 0) { 50 | return sizes 51 | } 52 | 53 | return sizes.filter((size) => { 54 | for (let filterSize of filterSizes) { 55 | if (size === filterSize) { 56 | return true 57 | } 58 | } 59 | 60 | return false 61 | }) 62 | } 63 | 64 | /** 65 | * Gets the size of the images needed to create an icon. 66 | * @param options Options from command line. 67 | * @return The sizes of the image. 68 | */ 69 | const getRequiredPNGImageSizes = (options: ICONOptions) => { 70 | let sizes: number[] = [] 71 | if (options.icns) { 72 | sizes = sizes.concat(filterSizes(ICNS_SIZES, options.icns.sizes)) 73 | } 74 | 75 | if (options.ico) { 76 | sizes = sizes.concat(filterSizes(ICO_SIZES, options.ico.sizes)) 77 | } 78 | 79 | if (options.favicon) { 80 | if (options.favicon.pngSizes) { 81 | // Favicon PNG generates the specified size as it is 82 | sizes = sizes.concat(options.favicon.pngSizes) 83 | } else { 84 | sizes = sizes.concat(FAV_SIZES) 85 | } 86 | } 87 | 88 | // 'all' mode 89 | if (sizes.length === 0) { 90 | sizes = FAV_SIZES.concat(ICNS_SIZES).concat(ICO_SIZES) 91 | } 92 | 93 | // Always ensure the ascending order 94 | return sizes 95 | .filter((value, index, array) => array.indexOf(value) === index) 96 | .sort((a, b) => a - b) 97 | } 98 | 99 | /** 100 | * Generate an icon files. 101 | * @param images Image file information. 102 | * @param dest Destination directory path. 103 | * @param options Options. 104 | * @param logger Logger. 105 | * @return Path of generated files. 106 | */ 107 | const generate = async ( 108 | images: ImageInfo[], 109 | dest: string, 110 | options: ICONOptions, 111 | logger: Logger 112 | ): Promise => { 113 | if (!(images && 0 < images.length)) { 114 | throw new Error('Targets is empty.') 115 | } 116 | 117 | const dir = path.resolve(dest) 118 | fs.mkdirSync(dir, {recursive: true}) 119 | 120 | const results: string[] = [] 121 | if (options.icns) { 122 | results.push(await generateICNS(images, dir, logger, options.icns)) 123 | } 124 | 125 | if (options.ico) { 126 | results.push(await generateICO(images, dir, logger, options.ico)) 127 | } 128 | 129 | if (options.favicon) { 130 | const files = await generateFavicon(images, dir, logger, options.favicon) 131 | for (const file of files) { 132 | results.push(file) 133 | } 134 | } 135 | 136 | return results 137 | } 138 | 139 | /** 140 | * Generate an icon from PNG file. 141 | * @param src Path of the PNG files directory. 142 | * @param dir Path of the output files directory. 143 | * @param options Options. 144 | * @param logger Logger. 145 | * @return Path of output files. 146 | */ 147 | const generateIconFromPNG = ( 148 | src: string, 149 | dir: string, 150 | options: ICONOptions, 151 | logger: Logger 152 | ): Promise => { 153 | const pngDirPath = path.resolve(src) 154 | const destDirPath = path.resolve(dir) 155 | logger.log('Icon generator from PNG:') 156 | logger.log(' src: ' + pngDirPath) 157 | logger.log(' dir: ' + destDirPath) 158 | 159 | const images = getRequiredPNGImageSizes(options) 160 | .map((size) => { 161 | return path.join(pngDirPath, size + '.png') 162 | }) 163 | .map((filePath) => { 164 | const size = Number(path.basename(filePath, '.png')) 165 | return { filePath, size } 166 | }) 167 | 168 | let notExistsFile = null 169 | images.some((image) => { 170 | const stat = fs.statSync(image.filePath) 171 | if (!(stat && stat.isFile())) { 172 | notExistsFile = path.basename(image.filePath) 173 | return true 174 | } 175 | 176 | return false 177 | }) 178 | 179 | if (notExistsFile) { 180 | throw new Error('"' + notExistsFile + '" does not exist.') 181 | } 182 | 183 | return generate(images, dir, options, logger) 184 | } 185 | 186 | /** 187 | * Generate an icon from SVG file. 188 | * @param src Path of the SVG file. 189 | * @param dir Path of the output files directory. 190 | * @param options Options from command line. 191 | * @param logger Logger. 192 | * @return Path of generated files. 193 | */ 194 | const generateIconFromSVG = async ( 195 | src: string, 196 | dir: string, 197 | options: ICONOptions, 198 | logger: Logger 199 | ): Promise => { 200 | const svgFilePath = path.resolve(src) 201 | const destDirPath = path.resolve(dir) 202 | logger.log('Icon generator from SVG:') 203 | logger.log(' src: ' + svgFilePath) 204 | logger.log(' dir: ' + destDirPath) 205 | 206 | const workDir = fs.mkdtempSync('icon-gen-') 207 | 208 | try { 209 | const images = await generatePNG( 210 | svgFilePath, 211 | workDir, 212 | getRequiredPNGImageSizes(options), 213 | logger 214 | ) 215 | const results = await generate(images, destDirPath, options, logger) 216 | return results 217 | } finally { 218 | fs.rmSync(workDir, {force: true, recursive: true}) 219 | } 220 | } 221 | 222 | /** 223 | * Generate an icon from SVG or PNG file. 224 | * @param src Path of the SVG file. 225 | * @param dest Path of the output files directory. 226 | * @param options Options. 227 | * @return Path of generated files. 228 | */ 229 | const generateIcon = async ( 230 | src: string, 231 | dest: string, 232 | options: ICONOptions = { ico: {}, icns: {}, favicon: {}, report: false } 233 | ): Promise => { 234 | if (!fs.existsSync(src)) { 235 | throw new Error('Input file or directory is not found.') 236 | } 237 | 238 | if (!fs.existsSync(dest)) { 239 | throw new Error('Output directory is not found.') 240 | } 241 | 242 | // Output all by default if no icon is specified 243 | if (!(options.ico || options.icns || options.favicon)) { 244 | options.ico = {} 245 | options.icns = {} 246 | options.favicon = {} 247 | } 248 | 249 | const logger = new Logger(options.report) 250 | if (fs.statSync(src).isDirectory()) { 251 | return generateIconFromPNG(src, dest, options, logger) 252 | } else { 253 | return generateIconFromSVG(src, dest, options, logger) 254 | } 255 | } 256 | 257 | export default generateIcon 258 | module.exports = generateIcon 259 | -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | /** Display the log message for the stdout. */ 2 | export default class Logger { 3 | private _available: boolean 4 | 5 | /** 6 | * Initialize instance. 7 | * @param available "true" to display the report, default is "false". 8 | */ 9 | constructor(available: boolean = false) { 10 | this._available = available 11 | } 12 | 13 | /** 14 | * Display a log message for the stdout. 15 | * @param args Message arguments. 16 | */ 17 | log(...args: any[]) { 18 | if (this._available) { 19 | console.log(...args) 20 | } 21 | } 22 | 23 | /** 24 | * Display an error message for the stdout. 25 | * @param args Message arguments. 26 | */ 27 | error(...args: any[]) { 28 | if (this._available) { 29 | console.error(...args) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/png.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import fs from 'node:fs' 3 | import Logger from './logger' 4 | import generatePNG, { filterImagesBySizes } from './png' 5 | import { REQUIRED_IMAGE_SIZES as FAV_SIZES } from './favicon' 6 | import { REQUIRED_IMAGE_SIZES as ICNS_SIZES } from './icns' 7 | import { REQUIRED_IMAGE_SIZES as ICO_SIZES } from './ico' 8 | 9 | test('generatePNG', () => { 10 | const dir = fs.mkdtempSync('icon-gen-') 11 | 12 | return generatePNG('./examples/data/sample.svg', dir, [16], new Logger()) 13 | .then((results) => { 14 | expect(results[0].size).toBe(16) 15 | }) 16 | .catch((err) => { 17 | console.error(err) 18 | }) 19 | .finally(() => { 20 | fs.rmSync(dir, { recursive: true, force: true }) 21 | }); 22 | }) 23 | 24 | // Test data 25 | const targets = ICO_SIZES.concat(ICNS_SIZES) 26 | .concat(FAV_SIZES) 27 | .filter((value, index, array) => array.indexOf(value) === index) 28 | .sort((a, b) => a - b) 29 | .map((size) => ({ size, filePath: '' })) 30 | 31 | test('filterImagesBySizes: ICO', () => { 32 | const sizes = filterImagesBySizes(targets, ICO_SIZES) 33 | expect(sizes.length).toBe(ICO_SIZES.length) 34 | }) 35 | 36 | test('filterImagesBySizes: ICNS', () => { 37 | const sizes = filterImagesBySizes(targets, ICNS_SIZES) 38 | expect(sizes.length).toBe(ICNS_SIZES.length) 39 | }) 40 | 41 | test('filterImagesBySizes: Favicon', () => { 42 | const sizes = filterImagesBySizes(targets, FAV_SIZES) 43 | expect(sizes.length).toBe(FAV_SIZES.length) 44 | }) 45 | -------------------------------------------------------------------------------- /src/lib/png.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import sharp from 'sharp' 4 | import Logger from './logger' 5 | 6 | /** Image file information. */ 7 | export type ImageInfo = { 8 | /** Image size (width/height). */ 9 | size: number 10 | /** Path of an image file. */ 11 | filePath: string 12 | } 13 | 14 | /** 15 | * Filter by size to the specified image information. 16 | * @param images Image file information. 17 | * @param sizes Required sizes. 18 | * @return Filtered image information. 19 | */ 20 | export const filterImagesBySizes = (images: ImageInfo[], sizes: number[]) => { 21 | return images 22 | .filter((image) => { 23 | return sizes.some((size) => { 24 | return image.size === size 25 | }) 26 | }) 27 | .sort((a, b) => { 28 | return a.size - b.size 29 | }) 30 | } 31 | 32 | /** 33 | * Generate the PNG file. 34 | * @param svg SVG data that has been parse by svg2png. 35 | * @param size The size (width/height) of the image. 36 | * @param dir Path of the file output directory. 37 | * @param logger Logger. 38 | * @return Image generation task. 39 | */ 40 | const generate = async ( 41 | svg: Buffer, 42 | size: number, 43 | dir: string, 44 | logger: Logger 45 | ): Promise => { 46 | const dest = path.join(dir, size + '.png') 47 | logger.log(' Create: ' + dest) 48 | 49 | await sharp(svg) 50 | .png({ compressionLevel: 9 }) 51 | .resize(size, size, { 52 | fit: 'contain', 53 | background: { r: 0, g: 0, b: 0, alpha: 0 } 54 | }) 55 | .toFile(dest) 56 | 57 | return { size: size, filePath: dest } 58 | } 59 | 60 | /** 61 | * Generate the PNG files. 62 | * @param src Path of SVG file. 63 | * @param dir Output destination The path of directory. 64 | * @param sizes Required PNG image size. 65 | * @param logger Logger. 66 | */ 67 | export const generatePNG = async ( 68 | src: string, 69 | dir: string, 70 | sizes: number[], 71 | logger: Logger 72 | ): Promise => { 73 | logger.log('SVG to PNG:') 74 | 75 | const svg = fs.readFileSync(src) 76 | const images: ImageInfo[] = [] 77 | for (const size of sizes) { 78 | images.push(await generate(svg, size, dir, logger)) 79 | } 80 | 81 | return images 82 | } 83 | 84 | export default generatePNG 85 | -------------------------------------------------------------------------------- /src/lib/rle.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import { packBits, packICNS, unpackBits, packBitsLiteralToResult } from './rle' 3 | 4 | test('packBits', () => { 5 | // Sample data : https://en.wikipedia.org/wiki/PackBits 6 | const src = [ 7 | 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 8 | 0x2a, 0x22, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa 9 | ] 10 | const expected = [ 11 | 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 12 | 0x22, 0xf7, 0xaa 13 | ] 14 | const actual = packBits(src) 15 | expect(actual).toStrictEqual(expected) 16 | }) 17 | 18 | test('packICNS', () => { 19 | const src = [0, 0, 0, 249, 250, 128, 100, 101] 20 | const actual = packICNS(src) 21 | const expected = [128, 0, 4, 249, 250, 128, 100, 101] 22 | expect(actual).toStrictEqual(expected) 23 | }) 24 | 25 | test('unpackBits', () => { 26 | // Sample data : https://en.wikipedia.org/wiki/PackBits 27 | const src = [ 28 | 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 29 | 0x22, 0xf7, 0xaa 30 | ] 31 | const expected = [ 32 | 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 33 | 0x2a, 0x22, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa 34 | ] 35 | const actual = unpackBits(src) 36 | expect(actual).toStrictEqual(expected) 37 | }) 38 | 39 | test('_packBitsLiteralToResult', () => { 40 | const actual = packBitsLiteralToResult([7, 1, 5, 8]) 41 | const expected = [3, 7, 1, 5, 8] 42 | expect(actual).toStrictEqual(expected) 43 | }) 44 | 45 | test('_packBitsLiteralToResult: Empty', () => { 46 | expect(packBitsLiteralToResult([])).toStrictEqual([]) 47 | }) 48 | -------------------------------------------------------------------------------- /src/lib/rle.ts: -------------------------------------------------------------------------------- 1 | /** Max length of PackBits literal. */ 2 | const MAX_LITERAL_LENGTH = 127 3 | 4 | /** 5 | * Copies the array to the target array at the specified position and size. 6 | * @param src Byte array of copy source. 7 | * @param srcBegin Copying start position of source. 8 | * @param dest Byte array of copy destination. 9 | * @param destBegin Writing start position of destination. 10 | * @param size Size of copy bytes. 11 | */ 12 | const arrayCopy = ( 13 | src: number[], 14 | srcBegin: number, 15 | dest: number[], 16 | destBegin: number, 17 | size: number 18 | ) => { 19 | if ( 20 | src.length <= srcBegin || 21 | src.length < size || 22 | dest.length <= destBegin || 23 | dest.length < size 24 | ) { 25 | return 26 | } 27 | 28 | for (let i = srcBegin, j = destBegin, k = 0; k < size; ++i, ++j, ++k) { 29 | dest[j] = src[i] 30 | } 31 | } 32 | 33 | /** 34 | * Convert a 8bit signed value to unsigned value. 35 | * @param value 8bit signed value (-127 to 127) 36 | * @return Unsigned value (0 to 255). 37 | */ 38 | const toUInt8 = (value: number) => { 39 | return value & 0xff 40 | } 41 | 42 | /** 43 | * Convert a 8bit unsigned value to signed value. 44 | * @param value 8bit unsigned value (0 to 255). 45 | * @return Signed value (-127 to 127). 46 | * @see https://github.com/inexorabletash/polyfill/blob/master/typedarray.js 47 | */ 48 | const toInt8 = (value: number) => { 49 | return (value << 24) >> 24 50 | } 51 | 52 | /** 53 | * Convert PackBits literals to results. 54 | * @param literals PackBits literals. 55 | * @return Converted literals. 56 | */ 57 | export const packBitsLiteralToResult = (literals: number[]) => { 58 | return literals.length === 0 59 | ? [] 60 | : [toUInt8(literals.length - 1)].concat(literals) 61 | } 62 | 63 | /** 64 | * Decompress PackBits compressed binary. 65 | * This method port Geeks with Blogs code (Apache License v2.0) to Node. 66 | * @param src Source binary. 67 | * @return Decompressed binary. 68 | * @see https://en.wikipedia.org/wiki/PackBits 69 | * @see http://geekswithblogs.net/rakker/archive/2015/12/14/packbits-in-c.aspx 70 | */ 71 | export const unpackBits = (src: number[]) => { 72 | const dest = [] 73 | for (let i = 0, max = src.length; i < max; ++i) { 74 | const count = toInt8(toUInt8(src[i])) 75 | if (count === -128) { 76 | // Do nothing, skip it 77 | } else if (0 <= count) { 78 | const total = count + 1 79 | for (let j = 0; j < total; ++j) { 80 | dest.push(toUInt8(src[i + j + 1])) 81 | } 82 | 83 | i += total 84 | } else { 85 | const total = Math.abs(count) + 1 86 | for (let j = 0; j < total; ++j) { 87 | dest.push(toUInt8(src[i + 1])) 88 | } 89 | 90 | ++i 91 | } 92 | } 93 | 94 | return dest 95 | } 96 | 97 | /** 98 | * Compress binary with ICNS RLE. 99 | * @param src Source binary. 100 | * @return Compressed binary. 101 | * @see https://github.com/fiji/IO/blob/master/src/main/java/sc/fiji/io/icns/RunLengthEncoding.java 102 | */ 103 | export const packICNS = (src: number[]) => { 104 | // If it is not redundant, keep the size large enough to increase the size 105 | const packedData = new Array(src.length * 2).fill(0) 106 | 107 | let output = 0 108 | for (let input = 0; input < src.length; ) { 109 | let literalStart = input 110 | let currentData = src[input++] 111 | 112 | // Read up to 128 literal bytes 113 | // Stop if 3 or more consecutive bytes are equal or EOF is reached 114 | let readBytes = 1 115 | let repeatedBytes = 0 116 | while (input < src.length && readBytes < 128 && repeatedBytes < 3) { 117 | const nextData = src[input++] 118 | if (nextData === currentData) { 119 | if (repeatedBytes === 0) { 120 | repeatedBytes = 2 121 | } else { 122 | repeatedBytes++ 123 | } 124 | } else { 125 | repeatedBytes = 0 126 | } 127 | 128 | readBytes++ 129 | currentData = nextData 130 | } 131 | 132 | let literalBytes = 0 133 | if (repeatedBytes < 3) { 134 | literalBytes = readBytes 135 | repeatedBytes = 0 136 | } else { 137 | literalBytes = readBytes - repeatedBytes 138 | } 139 | 140 | // Write the literal bytes that were read 141 | if (0 < literalBytes) { 142 | packedData[output++] = toUInt8(literalBytes - 1) 143 | arrayCopy(src, literalStart, packedData, output, literalBytes) 144 | output += literalBytes 145 | } 146 | 147 | // Read up to 130 consecutive bytes that are equal 148 | while ( 149 | input < src.length && 150 | src[input] === currentData && 151 | repeatedBytes < 130 152 | ) { 153 | repeatedBytes++ 154 | input++ 155 | } 156 | 157 | if (3 <= repeatedBytes) { 158 | // Write the repeated bytes if there are 3 or more 159 | packedData[output++] = toUInt8(repeatedBytes + 125) 160 | packedData[output++] = currentData 161 | } else { 162 | // Else move back the in pointer to ensure the repeated bytes are included in the next literal string 163 | input -= repeatedBytes 164 | } 165 | } 166 | 167 | // Trim to the actual size 168 | const dest = new Array(output).fill(0) 169 | arrayCopy(packedData, 0, dest, 0, output) 170 | 171 | return dest 172 | } 173 | 174 | /** 175 | * Compress binary with PackBits. 176 | * This method port Geeks with Blogs code (Apache License v2.0) to Node. 177 | * @param src Source binary. 178 | * @return Compressed binary. 179 | * @see https://en.wikipedia.org/wiki/PackBits 180 | * @see http://geekswithblogs.net/rakker/archive/2015/12/14/packbits-in-c.aspx 181 | */ 182 | export const packBits = (src: number[]) => { 183 | if (!(src && src.length && 0 < src.length)) { 184 | return [] 185 | } 186 | 187 | let dest: number[] = [] 188 | let literals = [] 189 | 190 | for (let i = 0, max = src.length; i < max; ++i) { 191 | const current = toUInt8(src[i]) 192 | if (i + 1 < max) { 193 | const next = toUInt8(src[i + 1]) 194 | if (current === next) { 195 | dest = dest.concat(packBitsLiteralToResult(literals)) 196 | literals = [] 197 | 198 | const maxJ = 199 | max <= i + MAX_LITERAL_LENGTH ? max - i - 1 : MAX_LITERAL_LENGTH 200 | let hitMax = true 201 | let runLength = 1 202 | 203 | for (let j = 2; j <= maxJ; ++j) { 204 | const run = src[i + j] 205 | if (current !== run) { 206 | hitMax = false 207 | const count = toUInt8(0 - runLength) 208 | i += j - 1 209 | dest.push(count) 210 | dest.push(current) 211 | break 212 | } 213 | 214 | ++runLength 215 | } 216 | 217 | if (hitMax) { 218 | dest.push(toUInt8(0 - maxJ)) 219 | dest.push(current) 220 | i += maxJ 221 | } 222 | } else { 223 | literals.push(current) 224 | if (literals.length === MAX_LITERAL_LENGTH) { 225 | dest = dest.concat(packBitsLiteralToResult(literals)) 226 | literals = [] 227 | } 228 | } 229 | } else { 230 | literals.push(current) 231 | dest = dest.concat(packBitsLiteralToResult(literals)) 232 | literals = [] 233 | } 234 | } 235 | 236 | return dest 237 | } 238 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "sourceMap": false 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["src/**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import 'sharp' 2 | --------------------------------------------------------------------------------