├── .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 | [![Coveralls github](https://img.shields.io/coveralls/github/etienne-martin/svg-to-img.svg)](https://coveralls.io/github/etienne-martin/svg-to-img) 7 | [![CircleCI build](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser.svg)](https://circleci.com/gh/etienne-martin/svg-to-img) 8 | [![node version](https://img.shields.io/node/v/svg-to-img.svg)](https://www.npmjs.com/package/svg-to-img) 9 | [![npm version](https://img.shields.io/npm/v/svg-to-img.svg)](https://www.npmjs.com/package/svg-to-img) 10 | [![npm monthly downloads](https://img.shields.io/npm/dm/svg-to-img.svg)](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 | 4 | 5 | 6 | 7 | 8 | digital-camera 9 | 10 | 11 | 12 | digital 13 | 14 | 11 15 | hardware 16 | photo 17 | digicam 18 | computer 19 | camera 20 | 21 | 22 | 23 | 24 | AJ Ashton 25 | 26 | 27 | 28 | 29 | AJ Ashton 30 | 31 | 32 | 33 | 34 | AJ Ashton 35 | 36 | 37 | 38 | image/svg+xml 39 | 40 | 41 | en 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /src/tests/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 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 | --------------------------------------------------------------------------------