├── .circleci
└── config.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── dist
├── constants.d.ts
├── constants.js
├── helpers.d.ts
├── helpers.js
├── index.d.ts
├── index.js
└── typings
│ └── index.d.ts
├── package-lock.json
├── package.json
├── src
├── constants.ts
├── helpers.ts
├── index.ts
├── tests
│ ├── helpers.test.ts
│ ├── helpers.ts
│ ├── index.test.ts
│ └── svg
│ │ ├── camera.svg
│ │ └── logo.svg
└── typings
│ └── index.ts
├── tsconfig.json
└── tslint.json
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | - image: circleci/node:8.9.4
10 |
11 | working_directory: ~/svg-to-img
12 |
13 | steps:
14 | - checkout
15 |
16 | # Download and cache dependencies
17 | - restore_cache:
18 | keys:
19 | - v1-dependencies-{{ checksum "package.json" }}
20 | # fallback to using the latest cache if no exact match is found
21 | - v1-dependencies-
22 |
23 | - run:
24 | name: Install node dependencies
25 | command: npm prune && npm install
26 |
27 | - run:
28 | name: Install puppeteer dependencies
29 | command: sudo apt-get update && sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
30 |
31 | - run:
32 | name: Build project
33 | command: npm run build
34 |
35 | - save_cache:
36 | paths:
37 | - node_modules
38 | key: v1-dependencies-{{ checksum "package.json" }}
39 |
40 | - run:
41 | name: Run tests
42 | command: npm run test
43 |
44 | - run:
45 | name: Check code coverage
46 | command: npm run coverage
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Optional npm cache directory
40 | .npm
41 |
42 | # Optional eslint cache
43 | .eslintcache
44 |
45 | # Optional REPL history
46 | .node_repl_history
47 |
48 | # Output of 'npm pack'
49 | *.tgz
50 |
51 | # Yarn Integrity file
52 | .yarn-integrity
53 |
54 | # dotenv environment variables file
55 | .env
56 |
57 | # Ignore intellij .idea folder
58 | .idea
59 |
60 | # Ignore test-generated images
61 | src/tests/img/*
62 |
63 | dist/typings/index.js
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | .idea
3 | tsconfig.json
4 | tslint.json
5 | coverage
6 | .circleci
7 | dist/typings/index.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Etienne Martin
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 | # svg-to-img
2 |
3 | #### A node.js library to convert SVGs to images built with [Puppeteer](https://github.com/GoogleChrome/puppeteer).
4 |
5 |
6 | [](https://coveralls.io/github/etienne-martin/svg-to-img)
7 | [](https://circleci.com/gh/etienne-martin/svg-to-img)
8 | [](https://www.npmjs.com/package/svg-to-img)
9 | [](https://www.npmjs.com/package/svg-to-img)
10 | [](https://www.npmjs.com/package/svg-to-img)
11 |
12 | ## Getting Started
13 |
14 | ### Installation
15 |
16 | To use svg-to-img in your project, run:
17 |
18 | ```bash
19 | npm install svg-to-img -S
20 | ```
21 |
22 | Note: When you install svg-to-img, it downloads a recent version of Chromium (~170Mb Mac, ~282Mb Linux, ~280Mb Win) that is guaranteed to work with the library.
23 |
24 | #### Debian
25 |
26 | If you're planning on running svg-to-img on Debian, you will need to manually install the following dependencies:
27 |
28 | ```bash
29 | #!/bin/bash
30 |
31 | apt-get update
32 | apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
33 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
34 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
35 | libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
36 | ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
37 | ```
38 |
39 | ### Usage
40 |
41 | Caution: svg-to-img uses async/await which is only supported in Node v7.6.0 or greater.
42 |
43 | **Example** - converting a `svg` to `png`:
44 |
45 | ```javascript
46 | const svgToImg = require("svg-to-img");
47 |
48 | (async () => {
49 | const image = await svgToImg.from("").toPng();
50 |
51 | console.log(image);
52 | })();
53 | ```
54 |
55 | **Example** - converting a `svg` to `jpeg` and saving the image as *example.jpeg*:
56 |
57 | ```javascript
58 | const svgToImg = require("svg-to-img");
59 |
60 | (async () => {
61 | await svgToImg.from("").toJpeg({
62 | path: "./example.jpeg"
63 | });
64 | })();
65 | ```
66 |
67 | **Example** - resizing a `svg` proportionally and converting it to `webp`:
68 |
69 | ```javascript
70 | const svgToImg = require("svg-to-img");
71 |
72 | (async () => {
73 | const image = await svgToImg.from("").toWebp({
74 | width: 300
75 | });
76 |
77 | console.log(image);
78 | })();
79 | ```
80 |
81 | **Example** - converting a `svg` to base64-encoded png:
82 |
83 | ```javascript
84 | const svgToImg = require("svg-to-img");
85 |
86 | (async () => {
87 | const image = await svgToImg.from("").toPng({
88 | encoding: "base64"
89 | });
90 |
91 | console.log(image);
92 | })();
93 | ```
94 |
95 | ## API Documentation
96 |
97 | ### svgToImg.from(svg)
98 | - `svg` SVG markup to be converted.
99 | - returns: <[Svg]> a new Svg object.
100 |
101 | The method returns a svg instance based on the given argument.
102 |
103 | ### svg.to([options])
104 | - `options` <[Object]> Options object which might have the following properties:
105 | - `path` <[string]> The file path to save the image to. The image type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
106 | - `type` <[string]> Specify image type, can be either `png`, `jpeg` or `webp`. Defaults to `png`.
107 | - `quality` <[number]> The quality of the image, between 0-1. Defaults to `1`. Not applicable to `png` images.
108 | - `width` <[number]> width of the output image. Defaults to the natural width of the SVG.
109 | - `height` <[number]> height of the output image. Defaults to the natural height of the SVG.
110 | - `clip` <[Object]> An object which specifies clipping region of the output image. Should have the following fields:
111 | - `x` <[number]> x-coordinate of top-left corner of clip area
112 | - `y` <[number]> y-coordinate of top-left corner of clip area
113 | - `width` <[number]> width of clipping area
114 | - `height` <[number]> height of clipping area
115 | - `background` <[string]> background color applied to the output image, must be a valid [CSS color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
116 | - `encoding` <[string]> Specify encoding, can be either `base64`, `utf8`, `binary` or `hex`. Returns a `Buffer` if this option is omitted.
117 | - returns: <[Promise]> Promise which resolves to the output image.
118 |
119 | ### svg.toPng([options])
120 | - `options` <[Object]> Optional options object that can have the same properties as the `to` method except for the type property.
121 | - returns: <[Promise]> Promise which resolves to the `png` image.
122 |
123 | This method is simply a shorthand for the `to` method.
124 |
125 | ### svg.toJpeg([options])
126 | - `options` <[Object]> Optional options object that can have the same properties as the `to` method except for the type property.
127 | - returns: <[Promise]> Promise which resolves to the `jpeg` image.
128 |
129 | This method is simply a shorthand for the `to` method.
130 |
131 | ### svg.toWebp([options])
132 | - `options` <[Object]> Optional options object that can have the same properties as the `to` method except for the type property.
133 | - returns: <[Promise]> Promise which resolves to the `webp` image.
134 |
135 | This method is simply a shorthand for the `to` method.
136 |
137 | ## Built with
138 |
139 | * [node.js](https://nodejs.org/en/) - Cross-platform JavaScript run-time environment for executing JavaScript code server-side.
140 | * [Puppeteer](https://github.com/GoogleChrome/puppeteer/) - Headless Chrome Node API.
141 | * [TypeScript](https://www.typescriptlang.org/) - Typed superset of JavaScript that compiles to plain JavaScript.
142 | * [Jest](https://facebook.github.io/jest/) - Delightful JavaScript Testing.
143 |
144 | ## Contributing
145 |
146 | When contributing to this project, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.
147 |
148 | Update the [README.md](https://github.com/etienne-martin/svg-to-img/blob/master/README.md) with details of changes to the library.
149 |
150 | Execute `npm run test` and update the [tests](https://github.com/etienne-martin/svg-to-img/tree/master/src/tests) if needed.
151 |
152 | ## Authors
153 |
154 | * **Etienne Martin** - *Initial work* - [etiennemartin.ca](http://etiennemartin.ca/)
155 |
156 | ## License
157 |
158 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/etienne-martin/svg-to-img/blob/master/LICENSE) file for details.
159 |
--------------------------------------------------------------------------------
/dist/constants.d.ts:
--------------------------------------------------------------------------------
1 | import { IOptions } from "./typings";
2 | export declare const config: {
3 | supportedImageTypes: string[];
4 | jpegBackground: string;
5 | puppeteer: {
6 | args: string[];
7 | };
8 | };
9 | export declare const defaultOptions: IOptions;
10 | export declare const defaultPngShorthandOptions: IOptions;
11 | export declare const defaultJpegShorthandOptions: IOptions;
12 | export declare const defaultWebpShorthandOptions: IOptions;
13 |
--------------------------------------------------------------------------------
/dist/constants.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.config = {
4 | supportedImageTypes: ["jpeg", "png", "webp"],
5 | jpegBackground: "#fff",
6 | puppeteer: {
7 | args: ["--no-sandbox", "--disable-setuid-sandbox"]
8 | }
9 | };
10 | exports.defaultOptions = {
11 | quality: 1,
12 | type: "png"
13 | };
14 | exports.defaultPngShorthandOptions = {
15 | type: "png"
16 | };
17 | exports.defaultJpegShorthandOptions = {
18 | type: "jpeg"
19 | };
20 | exports.defaultWebpShorthandOptions = {
21 | type: "webp"
22 | };
23 |
--------------------------------------------------------------------------------
/dist/helpers.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { IBoundingBox } from "./typings";
3 | export declare const getFileTypeFromPath: (path: string) => string;
4 | export declare const stringifyFunction: (func: any, ...argsArray: any[]) => string;
5 | export declare const writeFileAsync: (path: string, data: Buffer) => Promise<{}>;
6 | export declare const renderSvg: (svg: string, options: {
7 | width?: number | undefined;
8 | height?: number | undefined;
9 | type: "jpeg" | "png" | "webp" | undefined;
10 | quality: number | undefined;
11 | background?: string | undefined;
12 | clip?: IBoundingBox | undefined;
13 | jpegBackground: string;
14 | }) => Promise<{}>;
15 |
--------------------------------------------------------------------------------
/dist/helpers.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const fs = require("fs");
4 | exports.getFileTypeFromPath = (path) => {
5 | return path.toLowerCase().replace(new RegExp("jpg", "g"), "jpeg").split(".").reverse()[0];
6 | };
7 | exports.stringifyFunction = (func, ...argsArray) => {
8 | // Remove istanbul coverage instruments
9 | const functionString = func.toString().replace(/cov_(.+?)\+\+[,;]?/g, "");
10 | const args = [];
11 | for (const argument of argsArray) {
12 | switch (typeof argument) {
13 | case "string":
14 | args.push("`" + argument + "`");
15 | break;
16 | case "object":
17 | args.push(JSON.stringify(argument));
18 | break;
19 | default:
20 | args.push(argument);
21 | }
22 | }
23 | return `(${functionString})(${args.join(",")})`;
24 | };
25 | exports.writeFileAsync = async (path, data) => {
26 | return new Promise((resolve, reject) => {
27 | fs.writeFile(path, data, (err) => {
28 | if (err) {
29 | return reject(err);
30 | }
31 | resolve();
32 | });
33 | });
34 | };
35 | exports.renderSvg = async (svg, options) => {
36 | return new Promise((resolve, reject) => {
37 | const canvas = document.createElement("canvas");
38 | const ctx = canvas.getContext("2d");
39 | const img = new Image();
40 | /* istanbul ignore if */
41 | if (!ctx) {
42 | return reject(new Error("Canvas not supported"));
43 | }
44 | if (options.width) {
45 | img.width = options.width;
46 | }
47 | if (options.height) {
48 | img.height = options.height;
49 | }
50 | const onLoad = () => {
51 | let imageWidth = img.naturalWidth;
52 | let imageHeight = img.naturalHeight;
53 | if (options.width || options.height) {
54 | const computedStyle = window.getComputedStyle(img);
55 | imageWidth = parseInt(computedStyle.getPropertyValue("width"), 10);
56 | imageHeight = parseInt(computedStyle.getPropertyValue("height"), 10);
57 | }
58 | if (options.clip) {
59 | canvas.width = options.clip.width;
60 | canvas.height = options.clip.height;
61 | }
62 | else {
63 | canvas.width = imageWidth;
64 | canvas.height = imageHeight;
65 | }
66 | // Set default background color
67 | if (options.type === "jpeg") {
68 | ctx.fillStyle = options.jpegBackground;
69 | ctx.fillRect(0, 0, canvas.width, canvas.height);
70 | }
71 | // Set background color
72 | if (options.background) {
73 | ctx.fillStyle = options.background;
74 | ctx.fillRect(0, 0, canvas.width, canvas.height);
75 | }
76 | // Draw the image
77 | if (options.clip) {
78 | // Clipped image
79 | ctx.drawImage(img, options.clip.x, options.clip.y, options.clip.width, options.clip.height, 0, 0, options.clip.width, options.clip.height);
80 | }
81 | else {
82 | ctx.drawImage(img, 0, 0, imageWidth, imageHeight);
83 | }
84 | const dataURI = canvas.toDataURL("image/" + options.type, options.quality);
85 | const base64 = dataURI.substr(`data:image/${options.type};base64,`.length);
86 | document.body.removeChild(img);
87 | resolve(base64);
88 | };
89 | const onError = () => {
90 | document.body.removeChild(img);
91 | reject(new Error("Malformed SVG"));
92 | };
93 | img.addEventListener("load", onLoad);
94 | img.addEventListener("error", onError);
95 | document.body.appendChild(img);
96 | img.src = "data:image/svg+xml;charset=utf8," + svg;
97 | });
98 | };
99 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { IOptions, IShorthandOptions } from "./typings";
3 | export declare const from: (svg: string | Buffer) => {
4 | to: (options: IOptions) => Promise;
5 | toPng: (options?: IShorthandOptions | undefined) => Promise;
6 | toJpeg: (options?: IShorthandOptions | undefined) => Promise;
7 | toWebp: (options?: IShorthandOptions | undefined) => Promise;
8 | };
9 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const puppeteer = require("puppeteer");
4 | const helpers_1 = require("./helpers");
5 | const constants_1 = require("./constants");
6 | const queue = [];
7 | let browserDestructionTimeout;
8 | let browserInstance;
9 | let browserState = "closed";
10 | const executeQueuedRequests = (browser) => {
11 | for (const resolve of queue) {
12 | resolve(browser);
13 | }
14 | // Clear items from the queue
15 | queue.length = 0;
16 | };
17 | const getBrowser = async () => {
18 | return new Promise(async (resolve) => {
19 | clearTimeout(browserDestructionTimeout);
20 | if (browserState === "closed") {
21 | // Browser is closed
22 | queue.push(resolve);
23 | browserState = "opening";
24 | browserInstance = await puppeteer.launch(constants_1.config.puppeteer);
25 | browserState = "open";
26 | return executeQueuedRequests(browserInstance);
27 | }
28 | /* istanbul ignore if */
29 | if (browserState === "opening") {
30 | // Queue request and wait for the browser to open
31 | return queue.push(resolve);
32 | }
33 | /* istanbul ignore next */
34 | if (browserState === "open") {
35 | // Browser is already open
36 | if (browserInstance) {
37 | return resolve(browserInstance);
38 | }
39 | }
40 | });
41 | };
42 | const scheduleBrowserForDestruction = () => {
43 | clearTimeout(browserDestructionTimeout);
44 | browserDestructionTimeout = setTimeout(async () => {
45 | /* istanbul ignore next */
46 | if (browserInstance) {
47 | browserState = "closed";
48 | await browserInstance.close();
49 | }
50 | }, 500);
51 | };
52 | const convertSvg = async (inputSvg, passedOptions) => {
53 | const svg = Buffer.isBuffer(inputSvg) ? inputSvg.toString("utf8") : inputSvg;
54 | const options = Object.assign({}, constants_1.defaultOptions, passedOptions);
55 | const browser = await getBrowser();
56 | const page = (await browser.pages())[0];
57 | // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
58 | await page.setOfflineMode(true);
59 | // Infer the file type from the file path if no type is provided
60 | if (!passedOptions.type && options.path) {
61 | const fileType = helpers_1.getFileTypeFromPath(options.path);
62 | if (constants_1.config.supportedImageTypes.includes(fileType)) {
63 | options.type = fileType;
64 | }
65 | }
66 | const base64 = await page.evaluate(helpers_1.stringifyFunction(helpers_1.renderSvg, svg, {
67 | width: options.width,
68 | height: options.height,
69 | type: options.type,
70 | quality: options.quality,
71 | background: options.background,
72 | clip: options.clip,
73 | jpegBackground: constants_1.config.jpegBackground
74 | }));
75 | scheduleBrowserForDestruction();
76 | const buffer = Buffer.from(base64, "base64");
77 | if (options.path) {
78 | await helpers_1.writeFileAsync(options.path, buffer);
79 | }
80 | if (options.encoding === "base64") {
81 | return base64;
82 | }
83 | if (!options.encoding) {
84 | return buffer;
85 | }
86 | return buffer.toString(options.encoding);
87 | };
88 | const to = (svg) => {
89 | return async (options) => {
90 | return convertSvg(svg, options);
91 | };
92 | };
93 | const toPng = (svg) => {
94 | return async (options) => {
95 | return convertSvg(svg, Object.assign({}, constants_1.defaultPngShorthandOptions, options));
96 | };
97 | };
98 | const toJpeg = (svg) => {
99 | return async (options) => {
100 | return convertSvg(svg, Object.assign({}, constants_1.defaultJpegShorthandOptions, options));
101 | };
102 | };
103 | const toWebp = (svg) => {
104 | return async (options) => {
105 | return convertSvg(svg, Object.assign({}, constants_1.defaultWebpShorthandOptions, options));
106 | };
107 | };
108 | exports.from = (svg) => {
109 | return {
110 | to: to(svg),
111 | toPng: toPng(svg),
112 | toJpeg: toJpeg(svg),
113 | toWebp: toWebp(svg)
114 | };
115 | };
116 |
--------------------------------------------------------------------------------
/dist/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface IBoundingBox {
2 | x: number;
3 | y: number;
4 | width: number;
5 | height: number;
6 | }
7 | export interface IOptions {
8 | path?: string;
9 | type?: "jpeg" | "png" | "webp";
10 | quality?: number;
11 | width?: number;
12 | height?: number;
13 | clip?: IBoundingBox;
14 | background?: string;
15 | encoding?: "base64" | "utf8" | "binary" | "hex";
16 | }
17 | export interface IShorthandOptions extends IOptions {
18 | type?: never;
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svg-to-img",
3 | "version": "2.0.9",
4 | "description": "A node.js library to convert SVGs to images built with Puppeteer.",
5 | "homepage": "https://github.com/etienne-martin/svg-to-img",
6 | "keywords": [
7 | "svg",
8 | "png",
9 | "jpg",
10 | "jpeg",
11 | "image",
12 | "puppeteer",
13 | "node"
14 | ],
15 | "main": "dist/index.js",
16 | "types": "dist/index.d.ts",
17 | "author": {
18 | "name": "Etienne Martin",
19 | "url": "http://etiennemartin.ca/"
20 | },
21 | "scripts": {
22 | "dev": "tsc --pretty --watch",
23 | "lint": "tslint -c tslint.json -p tsconfig.json --fix",
24 | "pretest": "npm run lint",
25 | "test": "jest src --coverage --verbose",
26 | "test:watch": "jest src --coverage --verbose --watch",
27 | "coverage": "coveralls < ./coverage/lcov.info",
28 | "prebuild": "rm -rf dist/ && npm run lint",
29 | "build": "tsc --pretty",
30 | "prepare": "npm run build"
31 | },
32 | "husky": {
33 | "hooks": {
34 | "pre-commit": "npm run build && npm run test",
35 | "pre-push": "npm run build && npm run test"
36 | }
37 | },
38 | "jest": {
39 | "setupFiles": [
40 | "jest-canvas-mock"
41 | ],
42 | "transform": {
43 | "^.+\\.tsx?$": "ts-jest"
44 | },
45 | "moduleFileExtensions": [
46 | "ts",
47 | "tsx",
48 | "js",
49 | "jsx",
50 | "json",
51 | "node"
52 | ],
53 | "coveragePathIgnorePatterns": [
54 | "/node_modules/",
55 | "/coverage/",
56 | "/dist/",
57 | "/typings/",
58 | "/tests/"
59 | ],
60 | "coverageThreshold": {
61 | "global": {
62 | "branches": 100,
63 | "functions": 100,
64 | "lines": 100,
65 | "statements": -10
66 | }
67 | },
68 | "testMatch": [
69 | "**/?(*.)(test).(tsx|ts)"
70 | ],
71 | "collectCoverageFrom": [
72 | "src/**/*.(tsx|ts)"
73 | ],
74 | "testURL": "http://localhost"
75 | },
76 | "repository": {
77 | "type": "git",
78 | "url": "https://github.com/etienne-martin/svg-to-img"
79 | },
80 | "bugs": {
81 | "url": "https://github.com/etienne-martin/svg-to-img/issues"
82 | },
83 | "engines": {
84 | "node": ">= 7.6.0"
85 | },
86 | "license": "MIT",
87 | "devDependencies": {
88 | "@types/image-size": "0.0.29",
89 | "@types/jest": "22.2.3",
90 | "@types/puppeteer": "1.10.1",
91 | "@types/rimraf": "2.0.2",
92 | "coveralls": "3.0.2",
93 | "husky": "0.15.0-rc.13",
94 | "image-size": "0.6.3",
95 | "jest": "22.4.4",
96 | "jest-canvas-mock": "1.1.0",
97 | "rimraf": "2.6.2",
98 | "ts-jest": "22.4.6",
99 | "tslint": "5.11.0",
100 | "tslint-config-prettier": "1.17.0",
101 | "tslint-eslint-rules": "4.1.1",
102 | "typescript": "2.9.2"
103 | },
104 | "dependencies": {
105 | "puppeteer": "1.1.1"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { IOptions } from "./typings";
2 |
3 | export const config = {
4 | supportedImageTypes: ["jpeg", "png", "webp"],
5 | jpegBackground: "#fff",
6 | puppeteer: {
7 | args: ["--no-sandbox", "--disable-setuid-sandbox"]
8 | }
9 | };
10 |
11 | export const defaultOptions: IOptions = {
12 | quality: 1,
13 | type: "png"
14 | };
15 |
16 | export const defaultPngShorthandOptions: IOptions = {
17 | type: "png"
18 | };
19 |
20 | export const defaultJpegShorthandOptions: IOptions = {
21 | type: "jpeg"
22 | };
23 |
24 | export const defaultWebpShorthandOptions: IOptions = {
25 | type: "webp"
26 | };
27 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import { IBoundingBox, IOptions } from "./typings";
3 |
4 | export const getFileTypeFromPath = (path: string) => {
5 | return path.toLowerCase().replace(new RegExp("jpg", "g"), "jpeg").split(".").reverse()[0];
6 | };
7 |
8 | export const stringifyFunction = (func: any, ...argsArray: any[]) => {
9 | // Remove istanbul coverage instruments
10 | const functionString = func.toString().replace(/cov_(.+?)\+\+[,;]?/g, "");
11 | const args: Array = [];
12 |
13 | for (const argument of argsArray) {
14 | switch (typeof argument) {
15 | case "string":
16 | args.push("`" + argument + "`");
17 | break;
18 | case "object":
19 | args.push(JSON.stringify(argument));
20 | break;
21 | default:
22 | args.push(argument);
23 | }
24 | }
25 |
26 | return `(${functionString})(${args.join(",")})`;
27 | };
28 |
29 | export const writeFileAsync = async (path: string, data: Buffer) => {
30 | return new Promise((resolve, reject) => {
31 | fs.writeFile(path, data, (err: Error) => {
32 | if (err) { return reject(err); }
33 |
34 | resolve();
35 | });
36 | });
37 | };
38 |
39 | export const renderSvg = async (svg: string, options: {
40 | width?: IOptions["width"];
41 | height?: IOptions["height"];
42 | type: IOptions["type"];
43 | quality: IOptions["quality"];
44 | background?: IOptions["background"];
45 | clip?: IBoundingBox;
46 | jpegBackground: string;
47 | }) => {
48 | return new Promise((resolve, reject) => {
49 | const canvas = document.createElement("canvas");
50 | const ctx = canvas.getContext("2d");
51 | const img = new Image();
52 |
53 | /* istanbul ignore if */
54 | if (!ctx) {
55 | return reject(new Error("Canvas not supported"));
56 | }
57 |
58 | if (options.width) {
59 | img.width = options.width;
60 | }
61 |
62 | if (options.height) {
63 | img.height = options.height;
64 | }
65 |
66 | const onLoad = () => {
67 | let imageWidth = img.naturalWidth;
68 | let imageHeight = img.naturalHeight;
69 |
70 | if (options.width || options.height) {
71 | const computedStyle = window.getComputedStyle(img);
72 |
73 | imageWidth = parseInt(computedStyle.getPropertyValue("width"), 10);
74 | imageHeight = parseInt(computedStyle.getPropertyValue("height"), 10);
75 | }
76 |
77 | if (options.clip) {
78 | canvas.width = options.clip.width;
79 | canvas.height = options.clip.height;
80 | } else {
81 | canvas.width = imageWidth;
82 | canvas.height = imageHeight;
83 | }
84 |
85 | // Set default background color
86 | if (options.type === "jpeg") {
87 | ctx.fillStyle = options.jpegBackground;
88 | ctx.fillRect(0, 0, canvas.width, canvas.height);
89 | }
90 |
91 | // Set background color
92 | if (options.background) {
93 | ctx.fillStyle = options.background;
94 | ctx.fillRect(0, 0, canvas.width, canvas.height);
95 | }
96 |
97 | // Draw the image
98 | if (options.clip) {
99 | // Clipped image
100 | ctx.drawImage(
101 | img,
102 | options.clip.x,
103 | options.clip.y,
104 | options.clip.width,
105 | options.clip.height,
106 | 0,
107 | 0,
108 | options.clip.width,
109 | options.clip.height
110 | );
111 | } else {
112 | ctx.drawImage(img, 0, 0, imageWidth, imageHeight);
113 | }
114 |
115 | const dataURI = canvas.toDataURL("image/" + options.type, options.quality);
116 | const base64 = dataURI.substr(`data:image/${options.type};base64,`.length);
117 |
118 | document.body.removeChild(img);
119 | resolve(base64);
120 | };
121 |
122 | const onError = () => {
123 | document.body.removeChild(img);
124 | reject(new Error("Malformed SVG"));
125 | };
126 |
127 | img.addEventListener("load", onLoad);
128 | img.addEventListener("error", onError);
129 |
130 | document.body.appendChild(img);
131 | img.src = "data:image/svg+xml;charset=utf8," + svg;
132 | });
133 | };
134 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as puppeteer from "puppeteer";
2 | import { getFileTypeFromPath, renderSvg, stringifyFunction, writeFileAsync } from "./helpers";
3 | import { config, defaultOptions, defaultPngShorthandOptions, defaultJpegShorthandOptions, defaultWebpShorthandOptions } from "./constants";
4 | import { IOptions, IShorthandOptions } from "./typings";
5 |
6 | const queue: Array<(result: puppeteer.Browser) => void> = [];
7 | let browserDestructionTimeout: NodeJS.Timeout;
8 | let browserInstance: puppeteer.Browser|undefined;
9 | let browserState: "closed"|"opening"|"open" = "closed";
10 |
11 | const executeQueuedRequests = (browser: puppeteer.Browser) => {
12 | for (const resolve of queue) {
13 | resolve(browser);
14 | }
15 | // Clear items from the queue
16 | queue.length = 0;
17 | };
18 |
19 | const getBrowser = async (): Promise => {
20 | return new Promise(async (resolve: (result: puppeteer.Browser) => void) => {
21 | clearTimeout(browserDestructionTimeout);
22 |
23 | if (browserState === "closed") {
24 | // Browser is closed
25 | queue.push(resolve);
26 | browserState = "opening";
27 | browserInstance = await puppeteer.launch(config.puppeteer);
28 | browserState = "open";
29 |
30 | return executeQueuedRequests(browserInstance);
31 | }
32 |
33 | /* istanbul ignore if */
34 | if (browserState === "opening") {
35 | // Queue request and wait for the browser to open
36 | return queue.push(resolve);
37 | }
38 |
39 | /* istanbul ignore next */
40 | if (browserState === "open") {
41 | // Browser is already open
42 | if (browserInstance) {
43 | return resolve(browserInstance);
44 | }
45 | }
46 | });
47 | };
48 |
49 | const scheduleBrowserForDestruction = () => {
50 | clearTimeout(browserDestructionTimeout);
51 | browserDestructionTimeout = setTimeout(async () => {
52 | /* istanbul ignore next */
53 | if (browserInstance) {
54 | browserState = "closed";
55 | await browserInstance.close();
56 | }
57 | }, 500);
58 | };
59 |
60 | const convertSvg = async (inputSvg: Buffer|string, passedOptions: IOptions): Promise => {
61 | const svg = Buffer.isBuffer(inputSvg) ? (inputSvg as Buffer).toString("utf8") : inputSvg;
62 | const options = {...defaultOptions, ...passedOptions};
63 | const browser = await getBrowser();
64 | const page = (await browser.pages())[0];
65 |
66 | // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
67 | await page.setOfflineMode(true);
68 |
69 | // Infer the file type from the file path if no type is provided
70 | if (!passedOptions.type && options.path) {
71 | const fileType = getFileTypeFromPath(options.path);
72 |
73 | if (config.supportedImageTypes.includes(fileType)) {
74 | options.type = fileType as IOptions["type"];
75 | }
76 | }
77 |
78 | const base64 = await page.evaluate(stringifyFunction(renderSvg, svg, {
79 | width: options.width,
80 | height: options.height,
81 | type: options.type,
82 | quality: options.quality,
83 | background: options.background,
84 | clip: options.clip,
85 | jpegBackground: config.jpegBackground
86 | }));
87 |
88 | scheduleBrowserForDestruction();
89 |
90 | const buffer = Buffer.from(base64, "base64");
91 |
92 | if (options.path) {
93 | await writeFileAsync(options.path, buffer);
94 | }
95 |
96 | if (options.encoding === "base64") {
97 | return base64;
98 | }
99 |
100 | if (!options.encoding) {
101 | return buffer;
102 | }
103 |
104 | return buffer.toString(options.encoding);
105 | };
106 |
107 | const to = (svg: Buffer|string) => {
108 | return async (options: IOptions): Promise => {
109 | return convertSvg(svg, options);
110 | };
111 | };
112 |
113 | const toPng = (svg: Buffer|string) => {
114 | return async (options?: IShorthandOptions): Promise => {
115 | return convertSvg(svg, {...defaultPngShorthandOptions, ...options});
116 | };
117 | };
118 |
119 | const toJpeg = (svg: Buffer|string) => {
120 | return async (options?: IShorthandOptions): Promise => {
121 | return convertSvg(svg, {...defaultJpegShorthandOptions, ...options});
122 | };
123 | };
124 |
125 | const toWebp = (svg: Buffer|string) => {
126 | return async (options?: IShorthandOptions): Promise => {
127 | return convertSvg(svg, {...defaultWebpShorthandOptions, ...options});
128 | };
129 | };
130 |
131 | export const from = (svg: Buffer|string) => {
132 | return {
133 | to: to(svg),
134 | toPng: toPng(svg),
135 | toJpeg: toJpeg(svg),
136 | toWebp: toWebp(svg)
137 | };
138 | };
139 |
--------------------------------------------------------------------------------
/src/tests/helpers.test.ts:
--------------------------------------------------------------------------------
1 | import { getFileTypeFromPath, stringifyFunction, renderSvg, writeFileAsync } from "../helpers";
2 | import { config } from "../constants";
3 |
4 | beforeEach(() => {
5 | // Mock img.addEventListener("load|error", () => {});
6 | Element.prototype.addEventListener = jest.fn((event, callback) => {
7 | if (event === "load") {
8 | setTimeout(callback, 10);
9 | }
10 | });
11 | });
12 |
13 | describe("Helper functions", () => {
14 | test("Get file type from path", async () => {
15 | const fileType = await getFileTypeFromPath("test.jpg");
16 |
17 | expect(fileType).toBe("jpeg");
18 | });
19 |
20 | test("Stringify function", async () => {
21 | const func = stringifyFunction((str: string, obj: object, num: number) => str + obj + num, "a", {a: 1}, 1);
22 |
23 | expect(func).toEqual(`((str, obj, num) => str + obj + num)(\`a\`,{"a":1},1)`);
24 | });
25 |
26 | test("Render SVG with all options", async () => {
27 | const base64 = await renderSvg("", {
28 | width: 100,
29 | height: 100,
30 | type: "jpeg",
31 | quality: 1,
32 | background: "#09f",
33 | jpegBackground: config.jpegBackground
34 | });
35 |
36 | expect(base64).toBe("");
37 | });
38 |
39 | test("Render SVG with default options", async () => {
40 | const base64 = await renderSvg("", {
41 | type: "png",
42 | quality: 1,
43 | jpegBackground: config.jpegBackground
44 | });
45 |
46 | expect(base64).toBe("");
47 | });
48 |
49 | test("Render SVG with clipping options", async () => {
50 | const base64 = await renderSvg("", {
51 | clip: {
52 | x: 10,
53 | y: 10,
54 | width: 100,
55 | height: 100
56 | },
57 | type: "jpeg",
58 | quality: 1,
59 | jpegBackground: config.jpegBackground
60 | });
61 |
62 | expect(base64).toBe("");
63 | });
64 |
65 | test("Malformed SVG", async () => {
66 | Element.prototype.addEventListener = jest.fn((event, callback) => {
67 | if (event === "error") {
68 | setTimeout(callback, 10);
69 | }
70 | });
71 |
72 | try {
73 | await renderSvg("THIS IS NO SVG", {
74 | type: "png",
75 | quality: 1,
76 | jpegBackground: config.jpegBackground
77 | });
78 | } catch (error) {
79 | expect(error.message).toContain("Malformed SVG");
80 | }
81 | });
82 |
83 | test("Write file asynchronously", async () => {
84 | let errorThrown = false;
85 |
86 | try {
87 | await writeFileAsync("...//NOT-A-VALID-PATH//...", Buffer.from("dummy-data", "utf8"));
88 | } catch {
89 | errorThrown = true;
90 | }
91 |
92 | expect(errorThrown).toEqual(true);
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/tests/helpers.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from "crypto";
2 |
3 | export const md5 = (data: Buffer|string) => {
4 | return crypto.createHash("md5").update(data).digest("hex");
5 | };
6 |
--------------------------------------------------------------------------------
/src/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as svgToImg from "../index";
3 | import { md5 } from "./helpers";
4 | import * as rimraf from "rimraf";
5 | import * as sizeOf from "image-size";
6 |
7 | const inputDir = "./src/tests/svg";
8 | const outputDir = "./src/tests/img";
9 | const svgBuffer = fs.readFileSync(`${inputDir}/camera.svg`);
10 | const responsiveSvgBuffer = fs.readFileSync(`${inputDir}/logo.svg`);
11 | const svgString = svgBuffer.toString("utf8");
12 |
13 | // Create output directory
14 | rimraf.sync(outputDir);
15 | fs.mkdirSync(outputDir);
16 |
17 | describe("SVG to image conversion", () => {
18 | test("From buffer to image", async () => {
19 | const data = await svgToImg.from(svgBuffer).to({
20 | type: "jpeg"
21 | });
22 |
23 | expect(sizeOf(data as Buffer)).toEqual({
24 | type: "jpg",
25 | width: 406,
26 | height: 206
27 | });
28 | expect(md5(data)).toEqual("677e67f0c96c14a79032351d5691bcb2");
29 | });
30 |
31 | test("From string to image", async () => {
32 | const data = await svgToImg.from(svgString).to({
33 | type: "png"
34 | });
35 |
36 | expect(sizeOf(data as Buffer)).toEqual({
37 | type: "png",
38 | width: 406,
39 | height: 206
40 | });
41 | expect(md5(data)).toEqual("7c310bf3a7267c656d926ce5c8a1c365");
42 | });
43 |
44 | test("Infer file type from file extension", async () => {
45 | await svgToImg.from(svgBuffer).to({
46 | path: `${outputDir}/image.jpg`
47 | });
48 |
49 | const data = fs.readFileSync(`${outputDir}/image.jpg`);
50 |
51 | expect(sizeOf(data as Buffer)).toEqual({
52 | type: "jpg",
53 | width: 406,
54 | height: 206
55 | });
56 | expect(md5(data)).toEqual("677e67f0c96c14a79032351d5691bcb2");
57 | });
58 |
59 | test("Unknown file extension", async () => {
60 | await svgToImg.from(svgBuffer).to({
61 | path: `${outputDir}/image.ext`
62 | });
63 |
64 | const data = fs.readFileSync(`${outputDir}/image.ext`);
65 |
66 | expect(sizeOf(data as Buffer)).toEqual({
67 | type: "png",
68 | width: 406,
69 | height: 206
70 | });
71 | expect(md5(data)).toEqual("7c310bf3a7267c656d926ce5c8a1c365");
72 | });
73 |
74 | test("Base64 encoded output", async () => {
75 | const data = await svgToImg.from(svgString).to({
76 | encoding: "base64"
77 | });
78 |
79 | expect(sizeOf(Buffer.from(data as string, "base64"))).toEqual({
80 | type: "png",
81 | width: 406,
82 | height: 206
83 | });
84 | expect(md5(data)).toEqual("d8d4ae8a0824a579c7ca32a7ee93a678");
85 | });
86 |
87 | test("HEX encoded output", async () => {
88 | const data = await svgToImg.from(svgString).to({
89 | encoding: "hex"
90 | });
91 |
92 | expect(sizeOf(Buffer.from(data as string, "hex"))).toEqual({
93 | type: "png",
94 | width: 406,
95 | height: 206
96 | });
97 | expect(md5(data)).toEqual("dd8d4c070bb6db33ad15ace8dd56e61c");
98 | });
99 |
100 | test("JPEG compression", async () => {
101 | const data = await svgToImg.from(svgBuffer).toJpeg({
102 | quality: 0
103 | });
104 |
105 | expect(sizeOf(data as Buffer)).toEqual({
106 | type: "jpg",
107 | width: 406,
108 | height: 206
109 | });
110 | expect(md5(data)).toEqual("435447377ac681b187d8d55a65ea6b37");
111 | });
112 |
113 | test("WEBP compression", async () => {
114 | const data = await svgToImg.from(svgBuffer).toWebp({
115 | quality: 0
116 | });
117 |
118 | expect(sizeOf(data as Buffer)).toEqual({
119 | type: "webp",
120 | width: 406,
121 | height: 206
122 | });
123 | expect(md5(data)).toEqual("b5a88a19087b48e6aafacf688699ff0a");
124 | });
125 |
126 | test("Custom width and height", async () => {
127 | const data = await svgToImg.from(svgString).to({
128 | width: 1000,
129 | height: 200
130 | });
131 |
132 | expect(sizeOf(data as Buffer)).toEqual({
133 | type: "png",
134 | width: 1000,
135 | height: 200
136 | });
137 | expect(md5(data)).toEqual("35053a5b747abffa7cb1aba24bbbd603");
138 | });
139 |
140 | test("Custom background color", async () => {
141 | const data = await svgToImg.from(svgString).to({
142 | background: "#09f"
143 | });
144 |
145 | expect(sizeOf(data as Buffer)).toEqual({
146 | type: "png",
147 | width: 406,
148 | height: 206
149 | });
150 | expect(md5(data)).toEqual("f7c37d538eb948f6609d15d871b3f078");
151 | });
152 |
153 | test("Malformed SVG", async () => {
154 | try {
155 | await svgToImg.from("THIS IS NO SVG").to({
156 | type: "png"
157 | });
158 | } catch (error) {
159 | expect(error.message).toContain("Error: Malformed SVG");
160 | }
161 | });
162 |
163 | test("Responsive SVG (Infer natural dimensions)", async () => {
164 | const data = await svgToImg.from(responsiveSvgBuffer).toPng();
165 |
166 | expect(sizeOf(data as Buffer)).toEqual({
167 | type: "png",
168 | width: 187,
169 | height: 150
170 | });
171 | expect(md5(data)).toEqual("a35bb124b354bb861a6b65118ff16dde");
172 | });
173 |
174 | test("Resize responsive SVG (Squashed)", async () => {
175 | const data = await svgToImg.from(responsiveSvgBuffer).to({
176 | width: 300,
177 | height: 100
178 | });
179 |
180 | expect(sizeOf(data as Buffer)).toEqual({
181 | type: "png",
182 | width: 300,
183 | height: 100
184 | });
185 | expect(md5(data)).toEqual("f6571224da1e85780c7dc0ea66b7c95c");
186 | });
187 |
188 | test("Resize responsive SVG (Proportionally)", async () => {
189 | const data = await svgToImg.from(responsiveSvgBuffer).to({
190 | width: 300
191 | });
192 |
193 | expect(sizeOf(data as Buffer)).toEqual({
194 | type: "png",
195 | width: 300,
196 | height: 241
197 | });
198 | expect(md5(data)).toEqual("1245ca2a1868e5148d0bbeacc0245d25");
199 | });
200 |
201 | test("SVG to PNG shorthand", async () => {
202 | const data = await svgToImg.from(responsiveSvgBuffer).toPng();
203 |
204 | expect(sizeOf(data as Buffer)).toEqual({
205 | type: "png",
206 | width: 187,
207 | height: 150
208 | });
209 | expect(md5(data)).toEqual("a35bb124b354bb861a6b65118ff16dde");
210 | });
211 |
212 | test("SVG to JPEG shorthand", async () => {
213 | const data = await svgToImg.from(responsiveSvgBuffer).toJpeg();
214 |
215 | expect(sizeOf(data as Buffer)).toEqual({
216 | type: "jpg",
217 | width: 187,
218 | height: 150
219 | });
220 | expect(md5(data)).toEqual("da0a53cd944c1fbd56a64684969882cd");
221 | });
222 |
223 | test("SVG to WEBP shorthand", async () => {
224 | const data = await svgToImg.from(responsiveSvgBuffer).toWebp();
225 |
226 | expect(sizeOf(data as Buffer)).toEqual({
227 | type: "webp",
228 | width: 187,
229 | height: 150
230 | });
231 | expect(md5(data)).toEqual("b1080b283475987c0d57dd16a9f19288");
232 | });
233 |
234 | test("Clip the image", async () => {
235 | const data = await svgToImg.from(svgBuffer).toPng({
236 | clip: {
237 | x: 10,
238 | y: 10,
239 | width: 100,
240 | height: 100
241 | }
242 | });
243 |
244 | expect(sizeOf(data as Buffer)).toEqual({
245 | type: "png",
246 | width: 100,
247 | height: 100
248 | });
249 | expect(md5(data)).toEqual("68c1e882efb0a3ce1791e5a6e6b80bd7");
250 | });
251 |
252 | test("Wait for browser destruction", async (done) => {
253 | await svgToImg.from(responsiveSvgBuffer).toJpeg();
254 |
255 | setTimeout(async () => {
256 | done();
257 | }, 2000);
258 | });
259 |
260 | test("Test multiple requests in parallel", async (done) => {
261 | let errors = 0;
262 |
263 | for (let i = 0; i < 10; i++) {
264 | try {
265 | await svgToImg.from("").toPng();
266 | } catch (error) {
267 | console.log(error);
268 | errors++;
269 | }
270 | }
271 |
272 | setTimeout(() => {
273 | expect(errors).toBe(0);
274 | done();
275 | }, 1000);
276 | });
277 | });
278 |
279 | // Kill any remaining Chromium instances
280 | // pkill -f Chromium
281 |
--------------------------------------------------------------------------------
/src/tests/svg/camera.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/tests/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/typings/index.ts:
--------------------------------------------------------------------------------
1 | export interface IBoundingBox {
2 | x: number;
3 | y: number;
4 | width: number;
5 | height: number;
6 | }
7 |
8 | export interface IOptions {
9 | path?: string;
10 | type?: "jpeg" | "png" | "webp";
11 | quality?: number;
12 | width?: number;
13 | height?: number;
14 | clip?: IBoundingBox;
15 | background?: string;
16 | encoding?: "base64" | "utf8" | "binary" | "hex";
17 | }
18 |
19 | export interface IShorthandOptions extends IOptions {
20 | type?: never;
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es2017",
5 | "strict": true,
6 | "declaration": true,
7 | "noImplicitAny": true,
8 | "outDir": "./dist",
9 | "lib": [
10 | "dom",
11 | "es2017"
12 | ]
13 | },
14 | "include": [
15 | "src/**/*"
16 | ],
17 | "exclude": [
18 | "src/tests"
19 | ]
20 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended", "tslint-config-prettier"],
4 | "jsRules": {},
5 | "rules": {
6 | "one-variable-per-declaration": false,
7 | "quotemark": [true, "double"],
8 | "no-submodule-imports": false,
9 | "no-unused-variable": true,
10 | "promise-function-async": true,
11 | "max-file-line-count": [true, 300],
12 | "no-trailing-whitespace": true,
13 | "newline-before-return": true,
14 | "no-consecutive-blank-lines": true,
15 | "no-console": false,
16 | "object-literal-sort-keys": false,
17 | "ordered-imports": false
18 | },
19 | "rulesDirectory": []
20 | }
21 |
--------------------------------------------------------------------------------