Skip to content

Commit

Permalink
Merge pull request #8 from catdad/#7-generator
Browse files Browse the repository at this point in the history
add generator API for creating icons
  • Loading branch information
catdad authored Dec 19, 2023
2 parents cf1e9ea + c6c271f commit ffeafb0
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 124 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,35 @@ npm install svg-app-icon
## 👨‍💻 API

```javascript
const path = require('path');
const { promises: fs } = require('fs');
const icons = require('svg-app-icon');
const { generateIcons } = require('svg-app-icon');

(async () => {
const svg = await fs.readFile('my-icon.svg');

await icons(svg, {
destination: './my-output-directory'
});
for await (const icon of generateIcons(svg)) {
await fs.writeFile(path.resolve('./my-output-directory', icon.name), icon.buffer);
}
})();
```

### `icons(svgs, options)``Promise`
### `generateIcons(svgs, options)``AsyncGenerator`

The arguments for this method are:
* `svgs` _`String`|`Buffer`|`Array<String|Buffer>`_ - the SVG or SVG layers that you'd like to use as the icon. When multiple images are passed in, they will be layered on top of one another in the provided order
* `[options]` _`Object`_ - the options, everything is optional
* `[destination = 'icons']` _`String`_ - the directory to output all icons to. If this direcotry doesn't exist, it will be created
* `[icns = true]` _`Boolean`_ - whether to generate an ICNS icon for MacOS
* `[ico = true]` _`Boolean`_ - whether to generate an ICO icon for Windows
* `[png = true]` _`Boolean`_ - whether to generate all PNG icon sizes for Linux
* `[svg = true]` _`Boolean`_ - whether to generate output the original SVG to the output destination
* `[pngSizes = [32, 256, 512]]` _`Array<Integer>`_ - the sizes to output for PNG icons, in case you need any additional sizes

This promise resolves with `undefined`.
The [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) will yield `icon` opject. They contain the following properties:
* `name` _`String`_: the name of the file.
* `ext` _`String`_: the extension that should be used for the file. One of `['png', 'icns', 'ico']`
* `buffer` _`Buffer`_: the bytes of the generated icon file
* `size` _`Number`_: optional, only present for `png` icons, this is the size that was used to render the icon

## 💻 CLI

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "svg-app-icon",
"version": "1.2.0",
"description": "create high-quality desktop app icons for Windows, MacOS, and Linux using an SVG source",
"main": "src/maker.js",
"main": "src/index.js",
"bin": "bin/bin.js",
"scripts": {
"lint": "eslint bin/**/*.js src/**/*.js test/**/*.js",
Expand Down
102 changes: 102 additions & 0 deletions src/generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const renderSvg = require('svg-render');
const toIco = require('@catdad/to-ico');
const { Icns, IcnsImage } = require('@fiahfy/icns');
const { createCanvas, loadImage } = require('canvas');
const cheerio = require('cheerio');

const { toArray } = require('./helpers.js');

const createPng = async (buffers, size) => {
const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');

for (const buffer of buffers) {
const png = await renderSvg({ buffer, width: size, height: size });
const image = await loadImage(png);
ctx.drawImage(image, 0, 0);
}

return canvas.toBuffer('image/png');
};

const createIco = async svg => await toIco(
await Promise.all([16, 24, 32, 48, 64, 128, 256].map(size => createPng(svg, size)))
);

const createIcns = async svg => {
const icns = new Icns();

for (const { osType, size, format } of Icns.supportedIconTypes) {
if (format === 'PNG') {
icns.append(IcnsImage.fromPNG(await createPng(svg, size), osType));
}
}

return icns.data;
};

const createSvg = async svg => {
const size = 100;

const sizeNormalized = svg.map(s => {
const $ = cheerio.load(s.toString(), { xmlMode: true });
const $svg = $('svg');
$svg.attr('width', size);
$svg.attr('height', size);
$svg.attr('version', '1.1');
$svg.attr('xmlns', 'http://www.w3.org/2000/svg');

return $.xml('svg');
});

return [
`<svg viewBox="0 0 ${size} ${size}" version="1.1" xmlns="http://www.w3.org/2000/svg">`,
...sizeNormalized,
'</svg>'
].join('\n');
};

const getInputArray = input => {
return toArray(input).map(i => Buffer.isBuffer(i) ? i : Buffer.from(i));
};

async function* generateIcons(input, { icns = true, ico = true, png = true, svg = true, pngSizes = [32, 256, 512] } = {}) {
const buffers = getInputArray(input);

if (svg) {
yield {
name: 'icon.svg',
ext: 'svg',
buffer: Buffer.from(await createSvg(buffers))
};
}

if (ico) {
yield {
name: 'icon.ico',
ext: 'ico',
buffer: Buffer.from(await createIco(buffers))
};
}

if (icns) {
yield {
name: 'icon.icns',
ext: 'icns',
buffer: Buffer.from(await createIcns(buffers))
};
}

if (png) {
for (let size of pngSizes) {
yield {
name: `${size}x${size}.png`,
ext: 'png',
buffer: Buffer.from(await createPng(buffers, size)),
size
};
}
}
}

module.exports = generateIcons;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports = require('./maker.js');
module.exports.generateIcons = require('./generator.js');
82 changes: 5 additions & 77 deletions src/maker.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
const path = require('path');
const fs = require('fs').promises;

const renderSvg = require('svg-render');
const toIco = require('@catdad/to-ico');
const { Icns, IcnsImage } = require('@fiahfy/icns');
const { createCanvas, loadImage } = require('canvas');
const cheerio = require('cheerio');

const { toArray } = require('./helpers.js');
const generateIcons = require('./generator.js');

const dest = (...parts) => path.resolve('.', ...parts);

Expand All @@ -16,76 +10,10 @@ const write = async (dest, content) => {
await fs.writeFile(dest, content);
};

const createPng = async (buffers, size) => {
const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');

for (const buffer of buffers) {
const png = await renderSvg({ buffer, width: size, height: size });
const image = await loadImage(png);
ctx.drawImage(image, 0, 0);
const generateAndWriteToDisk = async (input, { destination = 'icons', ...options } = {}) => {
for await (const icon of generateIcons(input, options)) {
await write(dest(destination, icon.name), icon.buffer);
}

return canvas.toBuffer('image/png');
};

const createIco = async svg => await toIco(
await Promise.all([16, 24, 32, 48, 64, 128, 256].map(size => createPng(svg, size)))
);

const createIcns = async svg => {
const icns = new Icns();

for (const { osType, size } of Icns.supportedIconTypes) {
icns.append(IcnsImage.fromPNG(await createPng(svg, size), osType));
}

return icns.data;
};

const createSvg = async svg => {
const size = 100;

const sizeNormalized = svg.map(s => {
const $ = cheerio.load(s.toString(), { xmlMode: true });
const $svg = $('svg');
$svg.attr('width', size);
$svg.attr('height', size);
$svg.attr('version', '1.1');
$svg.attr('xmlns', 'http://www.w3.org/2000/svg');

return $.xml('svg');
});

return [
`<svg viewBox="0 0 ${size} ${size}" version="1.1" xmlns="http://www.w3.org/2000/svg">`,
...sizeNormalized,
'</svg>'
].join('\n');
};

const getInputArray = input => {
return toArray(input).map(i => Buffer.isBuffer(i) ? i : Buffer.from(i));
};

module.exports = async (input, { destination = 'icons', icns = true, ico = true, png = true, svg = true, pngSizes = [32, 256, 512] } = {}) => {
const buffers = getInputArray(input);

if (svg) {
await write(dest(destination, 'icon.svg'), await createSvg(buffers));
}

if (ico) {
await write(dest(destination, 'icon.ico'), await createIco(buffers));
}

if (icns) {
await write(dest(destination, 'icon.icns'), await createIcns(buffers));
}

if (png) {
for (let size of pngSizes) {
await write(dest(destination, `${size}x${size}.png`), await createPng(buffers, size));
}
}
};
module.exports = generateAndWriteToDisk;
22 changes: 7 additions & 15 deletions test/bin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const lodash = require('lodash');

const pkg = require('../package.json');
const binPath = path.resolve(__dirname, '..', pkg.bin);
const { validateIcons, svg, layers, png, type } = require('./helpers');
const { validateIconsDirectory, svg, layers, png, type, layerHashes } = require('./helpers');

const read = async stream => {
const content = [];
Expand Down Expand Up @@ -78,15 +78,15 @@ describe('app-icon-maker CLI', () => {

await runSuccess(destination);

await validateIcons(path.resolve(destination, 'icons'));
await validateIconsDirectory(path.resolve(destination, 'icons'));
});

it('optionally outputs to a custom destination', async () => {
destination = tempy.directory();

await runSuccess(destination, ['--destination', 'a/b/c']);

await validateIcons(path.resolve(destination, 'a/b/c'));
await validateIconsDirectory(path.resolve(destination, 'a/b/c'));
});

for (let include of ['icns', 'ico', 'png', 'svg']) {
Expand All @@ -102,7 +102,7 @@ describe('app-icon-maker CLI', () => {
svg: include === 'svg',
};

await validateIcons(path.resolve(destination, 'icons'), { ...expected });
await validateIconsDirectory(path.resolve(destination, 'icons'), { ...expected });
});
}

Expand All @@ -118,7 +118,7 @@ describe('app-icon-maker CLI', () => {
svg: true
};

await validateIcons(path.resolve(destination, 'icons'), { ...expected });
await validateIconsDirectory(path.resolve(destination, 'icons'), { ...expected });
});

it('can optionally generate a single arbitrary png size', async () => {
Expand Down Expand Up @@ -184,16 +184,8 @@ describe('app-icon-maker CLI', () => {

await runSuccess(destination, ['-i', 'png', '-i', 'svg', ...lodash.flatten(layerFiles)]);

const hashes = {
'32x32.png': 'c450e4c48d310cac5e1432dc3d8855b9a08da0c1e456eeacdbe4b809c8eb5b27',
'256x256.png': '7413a0717534701a7518a4e35633cae0edb63002c31ef58f092c555f2fa4bdfb',
// it's weird that these two are different?
'512x512.png': '926163d94eb5dd6309861db76e952d8562c83b815583440508f79b8213ed44b7',
'icon.svg': 'bba03b4311a86f6e6f6b7e8b37d444604bca27d95984bd56894ab98857a43cdf'
};

await validateIcons(path.resolve(destination, 'icons'), {
hashes,
await validateIconsDirectory(path.resolve(destination, 'icons'), {
hashes: layerHashes,
icns: false,
ico: false,
png: true,
Expand Down
Loading

0 comments on commit ffeafb0

Please sign in to comment.