diff --git a/README.md b/README.md
index d3cc776..874ccd3 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ To use svg-to-img in your project, run:
npm install svg-to-img -S
```
-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.
+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. If you want to skip downloading Chromium and instead [connect to a browser websocket endpoint](#svgtoimgconnectoptions), set the [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#environment-variables) environment variable.
#### Debian
@@ -92,6 +92,21 @@ const svgToImg = require("svg-to-img");
})();
```
+**Example** - connect to Puppeteer `browserWSEndpoint`:
+
+```javascript
+const svgToImg = require("svg-to-img");
+const convert = svgToImg.connect({
+ browserWSEndpoint: "ws://localhost:3000"
+});
+
+(async () => {
+ const image = await convert.from("").toPng();
+
+ console.log(image);
+})();
+```
+
## API Documentation
### svgToImg.from(svg)
@@ -100,6 +115,12 @@ const svgToImg = require("svg-to-img");
The method returns a svg instance based on the given argument.
+### svgToImg.connect(options)
+- `options` <[Object]> Pupetteer [connect options](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#puppeteerconnectoptions) object which will typically contain a `browserWSEndpoint` property.
+- returns: <[SvgToImg]> an instance which is connected with the specified Puppeteer endpoint.
+
+Use this method if you want to connect to a running Puppeteer endpoint (see [here](https://github.com/etienne-martin/svg-to-img/issues/9)). Note that the actual connection is established lazily; this means this method will always return, even when the given `browserWSEndpoint` cannot be reached -- in this case, errors will be thrown later when calling one of the `to` functions.
+
### svg.to([options])
- `options` <[Object]> Options object which might have the following properties:
- `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.
@@ -152,6 +173,7 @@ Execute `npm run test` and update the [tests](https://github.com/etienne-martin/
## Authors
* **Etienne Martin** - *Initial work* - [etiennemartin.ca](http://etiennemartin.ca/)
+* **Philipp Katz** - *Connect to browser endpoint* - [gitlab.com/qqilihq](https://gitlab.com/qqilihq)
## License
diff --git a/dist/helpers.js b/dist/helpers.js
index 8883174..6dfa37c 100644
--- a/dist/helpers.js
+++ b/dist/helpers.js
@@ -93,6 +93,14 @@ exports.renderSvg = async (svg, options) => {
img.addEventListener("load", onLoad);
img.addEventListener("error", onError);
document.body.appendChild(img);
- img.src = "data:image/svg+xml;charset=utf8," + svg;
+ // need to supply this base64-encoded, otherwise
+ // we get the "Malformed SVG" error (see above)
+ // when running remotely via WebSocket connection;
+ // note that `btoa` only works on ASCII characters,
+ // that’s why we need the `unescape(encodeURIComponent(…))`
+ // workaround -- see here:
+ // https://stackoverflow.com/a/26603875
+ const base64Svg = btoa(unescape(encodeURIComponent(svg)));
+ img.src = "data:image/svg+xml;base64," + base64Svg;
});
};
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 197d28f..ec3990e 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,8 +1,31 @@
///
-import { IOptions, IShorthandOptions } from "./typings";
-export declare const from: (svg: string | Buffer) => {
- to: (options: IOptions) => Promise;
- toPng: (options?: IShorthandOptions | undefined) => Promise;
- toJpeg: (options?: IShorthandOptions | undefined) => Promise;
- toWebp: (options?: IShorthandOptions | undefined) => Promise;
-};
+import * as puppeteer from "puppeteer";
+import { IOptions, IShorthandOptions, IConnectOptions } from "./typings";
+export declare class BrowserSource {
+ private readonly factory;
+ private queue;
+ private browserDestructionTimeout;
+ private browserInstance;
+ private browserState;
+ constructor(factory: () => Promise);
+ getBrowser(): Promise;
+ scheduleBrowserForDestruction(): void;
+ private executeQueuedRequests;
+}
+export declare class Svg {
+ private readonly svg;
+ private browserSource;
+ constructor(svg: Buffer | string, browserSource: BrowserSource);
+ to(options: IOptions): Promise;
+ toPng(options?: IShorthandOptions): Promise;
+ toJpeg(options?: IShorthandOptions): Promise;
+ toWebp(options?: IShorthandOptions): Promise;
+ private convertSvg;
+}
+export declare class SvgToImg {
+ private readonly browserSource;
+ constructor(browserSource: BrowserSource);
+ from(svg: Buffer | string): Svg;
+}
+export declare const from: (svg: string | Buffer) => Svg;
+export declare const connect: (options: IConnectOptions) => SvgToImg;
diff --git a/dist/index.js b/dist/index.js
index c909974..675c85f 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -3,113 +3,148 @@ Object.defineProperty(exports, "__esModule", { value: true });
const puppeteer = require("puppeteer");
const helpers_1 = require("./helpers");
const constants_1 = require("./constants");
-const queue = [];
-let browserDestructionTimeout;
-let browserInstance;
-let browserState = "closed";
-const executeQueuedRequests = (browser) => {
- for (const resolve of queue) {
- resolve(browser);
+class BrowserSource {
+ constructor(factory) {
+ this.factory = factory;
+ this.queue = [];
+ this.browserState = "closed";
}
- // Clear items from the queue
- queue.length = 0;
-};
-const getBrowser = async () => {
- return new Promise(async (resolve) => {
- clearTimeout(browserDestructionTimeout);
- if (browserState === "closed") {
- // Browser is closed
- queue.push(resolve);
- browserState = "opening";
- browserInstance = await puppeteer.launch(constants_1.config.puppeteer);
- browserState = "open";
- return executeQueuedRequests(browserInstance);
- }
+ async getBrowser() {
+ return new Promise(async (resolve, reject) => {
+ /* istanbul ignore if */
+ if (this.browserDestructionTimeout) {
+ clearTimeout(this.browserDestructionTimeout);
+ }
+ /* istanbul ignore else */
+ if (this.browserState === "closed") {
+ // Browser is closed
+ this.queue.push(resolve);
+ this.browserState = "opening";
+ try {
+ this.browserInstance = await this.factory();
+ this.browserState = "open";
+ return this.executeQueuedRequests(this.browserInstance);
+ }
+ catch (error) {
+ this.browserState = "closed";
+ return reject(error);
+ }
+ }
+ /* istanbul ignore next */
+ if (this.browserState === "opening") {
+ // Queue request and wait for the browser to open
+ return this.queue.push(resolve);
+ }
+ /* istanbul ignore next */
+ if (this.browserState === "open") {
+ // Browser is already open
+ if (this.browserInstance) {
+ return resolve(this.browserInstance);
+ }
+ }
+ });
+ }
+ ;
+ scheduleBrowserForDestruction() {
/* istanbul ignore if */
- if (browserState === "opening") {
- // Queue request and wait for the browser to open
- return queue.push(resolve);
+ if (this.browserDestructionTimeout) {
+ clearTimeout(this.browserDestructionTimeout);
}
- /* istanbul ignore next */
- if (browserState === "open") {
- // Browser is already open
- if (browserInstance) {
- return resolve(browserInstance);
+ this.browserDestructionTimeout = setTimeout(async () => {
+ /* istanbul ignore next */
+ if (this.browserInstance) {
+ this.browserState = "closed";
+ await this.browserInstance.close();
}
+ }, 500);
+ }
+ ;
+ executeQueuedRequests(browser) {
+ for (const resolve of this.queue) {
+ resolve(browser);
}
- });
-};
-const scheduleBrowserForDestruction = () => {
- clearTimeout(browserDestructionTimeout);
- browserDestructionTimeout = setTimeout(async () => {
- /* istanbul ignore next */
- if (browserInstance) {
- browserState = "closed";
- await browserInstance.close();
+ // Clear items from the queue
+ this.queue.length = 0;
+ }
+ ;
+}
+exports.BrowserSource = BrowserSource;
+;
+class Svg {
+ constructor(svg, browserSource) {
+ this.svg = svg;
+ this.browserSource = browserSource;
+ }
+ async to(options) {
+ return this.convertSvg(this.svg, options, this.browserSource);
+ }
+ ;
+ async toPng(options) {
+ return this.convertSvg(this.svg, Object.assign({}, constants_1.defaultPngShorthandOptions, options), this.browserSource);
+ }
+ ;
+ async toJpeg(options) {
+ return this.convertSvg(this.svg, Object.assign({}, constants_1.defaultJpegShorthandOptions, options), this.browserSource);
+ }
+ ;
+ async toWebp(options) {
+ return this.convertSvg(this.svg, Object.assign({}, constants_1.defaultWebpShorthandOptions, options), this.browserSource);
+ }
+ ;
+ async convertSvg(inputSvg, passedOptions, browserSource) {
+ const svg = Buffer.isBuffer(inputSvg) ? inputSvg.toString("utf8") : inputSvg;
+ const options = Object.assign({}, constants_1.defaultOptions, passedOptions);
+ const browser = await browserSource.getBrowser();
+ const page = (await browser.pages())[0];
+ // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
+ await page.setOfflineMode(true);
+ // Infer the file type from the file path if no type is provided
+ if (!passedOptions.type && options.path) {
+ const fileType = helpers_1.getFileTypeFromPath(options.path);
+ if (constants_1.config.supportedImageTypes.includes(fileType)) {
+ options.type = fileType;
+ }
}
- }, 500);
-};
-const convertSvg = async (inputSvg, passedOptions) => {
- const svg = Buffer.isBuffer(inputSvg) ? inputSvg.toString("utf8") : inputSvg;
- const options = Object.assign({}, constants_1.defaultOptions, passedOptions);
- const browser = await getBrowser();
- const page = (await browser.pages())[0];
- // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
- await page.setOfflineMode(true);
- // Infer the file type from the file path if no type is provided
- if (!passedOptions.type && options.path) {
- const fileType = helpers_1.getFileTypeFromPath(options.path);
- if (constants_1.config.supportedImageTypes.includes(fileType)) {
- options.type = fileType;
+ const base64 = await page.evaluate(helpers_1.stringifyFunction(helpers_1.renderSvg, svg, {
+ width: options.width,
+ height: options.height,
+ type: options.type,
+ quality: options.quality,
+ background: options.background,
+ clip: options.clip,
+ jpegBackground: constants_1.config.jpegBackground
+ }));
+ browserSource.scheduleBrowserForDestruction();
+ const buffer = Buffer.from(base64, "base64");
+ if (options.path) {
+ await helpers_1.writeFileAsync(options.path, buffer);
}
+ if (options.encoding === "base64") {
+ return base64;
+ }
+ if (!options.encoding) {
+ return buffer;
+ }
+ return buffer.toString(options.encoding);
}
- const base64 = await page.evaluate(helpers_1.stringifyFunction(helpers_1.renderSvg, svg, {
- width: options.width,
- height: options.height,
- type: options.type,
- quality: options.quality,
- background: options.background,
- clip: options.clip,
- jpegBackground: constants_1.config.jpegBackground
- }));
- scheduleBrowserForDestruction();
- const buffer = Buffer.from(base64, "base64");
- if (options.path) {
- await helpers_1.writeFileAsync(options.path, buffer);
- }
- if (options.encoding === "base64") {
- return base64;
+ ;
+}
+exports.Svg = Svg;
+class SvgToImg {
+ constructor(browserSource) {
+ this.browserSource = browserSource;
}
- if (!options.encoding) {
- return buffer;
+ from(svg) {
+ return new Svg(svg, this.browserSource);
}
- return buffer.toString(options.encoding);
-};
-const to = (svg) => {
- return async (options) => {
- return convertSvg(svg, options);
- };
-};
-const toPng = (svg) => {
- return async (options) => {
- return convertSvg(svg, Object.assign({}, constants_1.defaultPngShorthandOptions, options));
- };
-};
-const toJpeg = (svg) => {
- return async (options) => {
- return convertSvg(svg, Object.assign({}, constants_1.defaultJpegShorthandOptions, options));
- };
-};
-const toWebp = (svg) => {
- return async (options) => {
- return convertSvg(svg, Object.assign({}, constants_1.defaultWebpShorthandOptions, options));
- };
-};
+ ;
+}
+exports.SvgToImg = SvgToImg;
+const defaultBrowserSource = new BrowserSource(async () => puppeteer.launch(constants_1.config.puppeteer));
exports.from = (svg) => {
- return {
- to: to(svg),
- toPng: toPng(svg),
- toJpeg: toJpeg(svg),
- toWebp: toWebp(svg)
- };
+ return new SvgToImg(defaultBrowserSource).from(svg);
+};
+/* istanbul ignore next */
+exports.connect = (options) => {
+ return new SvgToImg(new BrowserSource(async () => puppeteer.connect(options)));
};
diff --git a/dist/typings/index.d.ts b/dist/typings/index.d.ts
index a255078..2a5acbb 100644
--- a/dist/typings/index.d.ts
+++ b/dist/typings/index.d.ts
@@ -17,3 +17,8 @@ export interface IOptions {
export interface IShorthandOptions extends IOptions {
type?: never;
}
+export interface IConnectOptions {
+ browserWSEndpoint?: string;
+ browserURL?: string;
+ ignoreHTTPSErrors?: boolean;
+}
diff --git a/package-lock.json b/package-lock.json
index cb61c43..b6bcc2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -132,9 +132,9 @@
"dev": true
},
"agent-base": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
- "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+ "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
@@ -1405,18 +1405,11 @@
}
},
"debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
- "ms": "2.0.0"
- },
- "dependencies": {
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
- }
+ "ms": "^2.1.1"
}
},
"decamelize": {
@@ -1629,13 +1622,13 @@
}
},
"es6-promise": {
- "version": "4.2.5",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
- "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg=="
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
- "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
@@ -1800,6 +1793,21 @@
"debug": "2.6.9",
"mkdirp": "0.5.1",
"yauzl": "2.4.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
}
},
"extsprintf": {
@@ -2753,11 +2761,11 @@
}
},
"https-proxy-agent": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
- "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
+ "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
"requires": {
- "agent-base": "^4.1.0",
+ "agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
@@ -3881,9 +3889,9 @@
}
},
"mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+ "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
},
"mime-db": {
"version": "1.37.0",
@@ -4470,18 +4478,18 @@
"dev": true
},
"puppeteer": {
- "version": "1.1.1",
- "resolved": "http://registry.npmjs.org/puppeteer/-/puppeteer-1.1.1.tgz",
- "integrity": "sha1-rb8l5J9e8DRDwQq44JqVTKDHv+4=",
- "requires": {
- "debug": "^2.6.8",
- "extract-zip": "^1.6.5",
- "https-proxy-agent": "^2.1.0",
- "mime": "^1.3.4",
- "progress": "^2.0.0",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.0.0.tgz",
+ "integrity": "sha512-t3MmTWzQxPRP71teU6l0jX47PHXlc4Z52sQv4LJQSZLq1ttkKS2yGM3gaI57uQwZkNaoGd0+HPPMELZkcyhlqA==",
+ "requires": {
+ "debug": "^4.1.0",
+ "extract-zip": "^1.6.6",
+ "https-proxy-agent": "^3.0.0",
+ "mime": "^2.0.3",
+ "progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^2.6.1",
- "ws": "^3.0.0"
+ "ws": "^6.1.0"
}
},
"qs": {
@@ -6166,11 +6174,6 @@
}
}
},
- "ultron": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
- "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
- },
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
@@ -6468,13 +6471,11 @@
}
},
"ws": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
- "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
+ "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"requires": {
- "async-limiter": "~1.0.0",
- "safe-buffer": "~5.1.0",
- "ultron": "~1.1.0"
+ "async-limiter": "~1.0.0"
}
},
"xml-name-validator": {
diff --git a/package.json b/package.json
index 4aa9c1d..d1a392f 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,9 @@
"name": "Etienne Martin",
"url": "http://etiennemartin.ca/"
},
+ "contributors": [
+ "Philipp Katz (https://gitlab.com/qqilihq)"
+ ],
"scripts": {
"dev": "tsc --pretty --watch",
"lint": "tslint -c tslint.json -p tsconfig.json --fix",
@@ -102,6 +105,6 @@
"typescript": "2.9.2"
},
"dependencies": {
- "puppeteer": "1.1.1"
+ "puppeteer": "2.0.0"
}
}
diff --git a/src/helpers.ts b/src/helpers.ts
index 74740df..a77fac5 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -28,7 +28,7 @@ export const stringifyFunction = (func: any, ...argsArray: any[]) => {
export const writeFileAsync = async (path: string, data: Buffer) => {
return new Promise((resolve, reject) => {
- fs.writeFile(path, data, (err: Error) => {
+ fs.writeFile(path, data, (err: NodeJS.ErrnoException | null) => {
if (err) { return reject(err); }
resolve();
@@ -128,6 +128,14 @@ export const renderSvg = async (svg: string, options: {
img.addEventListener("error", onError);
document.body.appendChild(img);
- img.src = "data:image/svg+xml;charset=utf8," + svg;
+ // need to supply this base64-encoded, otherwise
+ // we get the "Malformed SVG" error (see above)
+ // when running remotely via WebSocket connection;
+ // note that `btoa` only works on ASCII characters,
+ // that’s why we need the `unescape(encodeURIComponent(…))`
+ // workaround -- see here:
+ // https://stackoverflow.com/a/26603875
+ const base64Svg = btoa(unescape(encodeURIComponent(svg)));
+ img.src = "data:image/svg+xml;base64," + base64Svg;
});
};
diff --git a/src/index.ts b/src/index.ts
index c8c0631..735186a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,138 +1,161 @@
import * as puppeteer from "puppeteer";
import { getFileTypeFromPath, renderSvg, stringifyFunction, writeFileAsync } from "./helpers";
import { config, defaultOptions, defaultPngShorthandOptions, defaultJpegShorthandOptions, defaultWebpShorthandOptions } from "./constants";
-import { IOptions, IShorthandOptions } from "./typings";
-
-const queue: Array<(result: puppeteer.Browser) => void> = [];
-let browserDestructionTimeout: NodeJS.Timeout;
-let browserInstance: puppeteer.Browser|undefined;
-let browserState: "closed"|"opening"|"open" = "closed";
-
-const executeQueuedRequests = (browser: puppeteer.Browser) => {
- for (const resolve of queue) {
- resolve(browser);
- }
- // Clear items from the queue
- queue.length = 0;
-};
+import { IOptions, IShorthandOptions, IConnectOptions } from "./typings";
-const getBrowser = async (): Promise => {
- return new Promise(async (resolve: (result: puppeteer.Browser) => void) => {
- clearTimeout(browserDestructionTimeout);
+export class BrowserSource {
+ private queue: Array<(result: puppeteer.Browser) => void> = [];
+ private browserDestructionTimeout: NodeJS.Timeout | undefined;
+ private browserInstance: puppeteer.Browser | undefined;
+ private browserState: "closed" | "opening" | "open" = "closed";
- if (browserState === "closed") {
- // Browser is closed
- queue.push(resolve);
- browserState = "opening";
- browserInstance = await puppeteer.launch(config.puppeteer);
- browserState = "open";
+ constructor (private readonly factory: () => Promise) {}
- return executeQueuedRequests(browserInstance);
- }
+ public async getBrowser (): Promise {
+ return new Promise(async (resolve: (result: puppeteer.Browser) => void, reject: (err: any) => void) => {
+ /* istanbul ignore if */
+ if (this.browserDestructionTimeout) {
+ clearTimeout(this.browserDestructionTimeout);
+ }
- /* istanbul ignore if */
- if (browserState === "opening") {
- // Queue request and wait for the browser to open
- return queue.push(resolve);
- }
+ /* istanbul ignore else */
+ if (this.browserState === "closed") {
+ // Browser is closed
+ this.queue.push(resolve);
+ this.browserState = "opening";
+ try {
+ this.browserInstance = await this.factory();
+ this.browserState = "open";
+
+ return this.executeQueuedRequests(this.browserInstance);
+ } catch (error) {
+ this.browserState = "closed";
+
+ return reject(error);
+ }
+ }
+
+ /* istanbul ignore next */
+ if (this.browserState === "opening") {
+ // Queue request and wait for the browser to open
+ return this.queue.push(resolve);
+ }
- /* istanbul ignore next */
- if (browserState === "open") {
- // Browser is already open
- if (browserInstance) {
- return resolve(browserInstance);
+ /* istanbul ignore next */
+ if (this.browserState === "open") {
+ // Browser is already open
+ if (this.browserInstance) {
+ return resolve(this.browserInstance);
+ }
}
+ });
+ };
+
+ public scheduleBrowserForDestruction () {
+ /* istanbul ignore if */
+ if (this.browserDestructionTimeout) {
+ clearTimeout(this.browserDestructionTimeout);
}
- });
-};
+ this.browserDestructionTimeout = setTimeout(async () => {
+ /* istanbul ignore next */
+ if (this.browserInstance) {
+ this.browserState = "closed";
+ await this.browserInstance.close();
+ }
+ }, 500);
+ };
-const scheduleBrowserForDestruction = () => {
- clearTimeout(browserDestructionTimeout);
- browserDestructionTimeout = setTimeout(async () => {
- /* istanbul ignore next */
- if (browserInstance) {
- browserState = "closed";
- await browserInstance.close();
+ private executeQueuedRequests (browser: puppeteer.Browser) {
+ for (const resolve of this.queue) {
+ resolve(browser);
}
- }, 500);
+ // Clear items from the queue
+ this.queue.length = 0;
+ };
};
-const convertSvg = async (inputSvg: Buffer|string, passedOptions: IOptions): Promise => {
- const svg = Buffer.isBuffer(inputSvg) ? (inputSvg as Buffer).toString("utf8") : inputSvg;
- const options = {...defaultOptions, ...passedOptions};
- const browser = await getBrowser();
- const page = (await browser.pages())[0];
+export class Svg {
- // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
- await page.setOfflineMode(true);
+ constructor (private readonly svg: Buffer|string, private browserSource: BrowserSource) {}
- // Infer the file type from the file path if no type is provided
- if (!passedOptions.type && options.path) {
- const fileType = getFileTypeFromPath(options.path);
+ public async to (options: IOptions): Promise {
+ return this.convertSvg(this.svg, options, this.browserSource);
+ };
- if (config.supportedImageTypes.includes(fileType)) {
- options.type = fileType as IOptions["type"];
- }
- }
+ public async toPng (options?: IShorthandOptions): Promise {
+ return this.convertSvg(this.svg, {...defaultPngShorthandOptions, ...options}, this.browserSource);
+ };
- const base64 = await page.evaluate(stringifyFunction(renderSvg, svg, {
- width: options.width,
- height: options.height,
- type: options.type,
- quality: options.quality,
- background: options.background,
- clip: options.clip,
- jpegBackground: config.jpegBackground
- }));
+ public async toJpeg (options?: IShorthandOptions): Promise {
+ return this.convertSvg(this.svg, {...defaultJpegShorthandOptions, ...options}, this.browserSource);
+ };
- scheduleBrowserForDestruction();
+ public async toWebp (options?: IShorthandOptions): Promise {
+ return this.convertSvg(this.svg, {...defaultWebpShorthandOptions, ...options}, this.browserSource);
+ };
- const buffer = Buffer.from(base64, "base64");
+ private async convertSvg (inputSvg: Buffer|string, passedOptions: IOptions, browserSource: BrowserSource): Promise {
+ const svg = Buffer.isBuffer(inputSvg) ? (inputSvg as Buffer).toString("utf8") : inputSvg;
+ const options = {...defaultOptions, ...passedOptions};
+ const browser = await browserSource.getBrowser();
+ const page = (await browser.pages())[0];
- if (options.path) {
- await writeFileAsync(options.path, buffer);
- }
+ // ⚠️ Offline mode is enabled to prevent any HTTP requests over the network
+ await page.setOfflineMode(true);
- if (options.encoding === "base64") {
- return base64;
- }
+ // Infer the file type from the file path if no type is provided
+ if (!passedOptions.type && options.path) {
+ const fileType = getFileTypeFromPath(options.path);
- if (!options.encoding) {
- return buffer;
- }
+ if (config.supportedImageTypes.includes(fileType)) {
+ options.type = fileType as IOptions["type"];
+ }
+ }
- return buffer.toString(options.encoding);
-};
+ const base64 = await page.evaluate(stringifyFunction(renderSvg, svg, {
+ width: options.width,
+ height: options.height,
+ type: options.type,
+ quality: options.quality,
+ background: options.background,
+ clip: options.clip,
+ jpegBackground: config.jpegBackground
+ }));
-const to = (svg: Buffer|string) => {
- return async (options: IOptions): Promise => {
- return convertSvg(svg, options);
- };
-};
+ browserSource.scheduleBrowserForDestruction();
-const toPng = (svg: Buffer|string) => {
- return async (options?: IShorthandOptions): Promise => {
- return convertSvg(svg, {...defaultPngShorthandOptions, ...options});
- };
-};
+ const buffer = Buffer.from(base64, "base64");
-const toJpeg = (svg: Buffer|string) => {
- return async (options?: IShorthandOptions): Promise => {
- return convertSvg(svg, {...defaultJpegShorthandOptions, ...options});
+ if (options.path) {
+ await writeFileAsync(options.path, buffer);
+ }
+
+ if (options.encoding === "base64") {
+ return base64;
+ }
+
+ if (!options.encoding) {
+ return buffer;
+ }
+
+ return buffer.toString(options.encoding);
};
-};
+}
-const toWebp = (svg: Buffer|string) => {
- return async (options?: IShorthandOptions): Promise => {
- return convertSvg(svg, {...defaultWebpShorthandOptions, ...options});
+export class SvgToImg {
+ constructor (private readonly browserSource: BrowserSource) {}
+ public from (svg: Buffer|string) {
+ return new Svg(svg, this.browserSource);
};
-};
+}
+
+const defaultBrowserSource = new BrowserSource(async () => puppeteer.launch(config.puppeteer));
export const from = (svg: Buffer|string) => {
- return {
- to: to(svg),
- toPng: toPng(svg),
- toJpeg: toJpeg(svg),
- toWebp: toWebp(svg)
- };
-};
+ return new SvgToImg(defaultBrowserSource).from(svg);
+}
+
+/* istanbul ignore next */
+export const connect = (options: IConnectOptions) => {
+ return new SvgToImg(new BrowserSource(async () => puppeteer.connect(options)));
+}
diff --git a/src/tests/index.test.ts b/src/tests/index.test.ts
index 800ea48..3fa1fe6 100644
--- a/src/tests/index.test.ts
+++ b/src/tests/index.test.ts
@@ -25,7 +25,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("677e67f0c96c14a79032351d5691bcb2");
+ expect(md5(data)).toEqual("242fac8e57f2c24e6865733c78ffd49a");
});
test("From string to image", async () => {
@@ -38,7 +38,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("7c310bf3a7267c656d926ce5c8a1c365");
+ expect(md5(data)).toEqual("d7f42c771389e20ef07397ebcd3aa5ac");
});
test("Infer file type from file extension", async () => {
@@ -53,7 +53,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("677e67f0c96c14a79032351d5691bcb2");
+ expect(md5(data)).toEqual("242fac8e57f2c24e6865733c78ffd49a");
});
test("Unknown file extension", async () => {
@@ -68,7 +68,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("7c310bf3a7267c656d926ce5c8a1c365");
+ expect(md5(data)).toEqual("d7f42c771389e20ef07397ebcd3aa5ac");
});
test("Base64 encoded output", async () => {
@@ -81,7 +81,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("d8d4ae8a0824a579c7ca32a7ee93a678");
+ expect(md5(data)).toEqual("de0dcfb7ab63a50140c2ab562bd2d942");
});
test("HEX encoded output", async () => {
@@ -94,7 +94,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("dd8d4c070bb6db33ad15ace8dd56e61c");
+ expect(md5(data)).toEqual("492efd51b52dc7376343202ee67225ca");
});
test("JPEG compression", async () => {
@@ -107,7 +107,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("435447377ac681b187d8d55a65ea6b37");
+ expect(md5(data)).toEqual("2b6a20b486cf02671c13c0cb9e6bac1d");
});
test("WEBP compression", async () => {
@@ -120,7 +120,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("b5a88a19087b48e6aafacf688699ff0a");
+ expect(md5(data)).toEqual("aef2e325aa30cd0299ee95c56ff49b9c");
});
test("Custom width and height", async () => {
@@ -134,7 +134,7 @@ describe("SVG to image conversion", () => {
width: 1000,
height: 200
});
- expect(md5(data)).toEqual("35053a5b747abffa7cb1aba24bbbd603");
+ expect(md5(data)).toEqual("c683880ab02c0e76243d0abaceffe0c8");
});
test("Custom background color", async () => {
@@ -147,7 +147,7 @@ describe("SVG to image conversion", () => {
width: 406,
height: 206
});
- expect(md5(data)).toEqual("f7c37d538eb948f6609d15d871b3f078");
+ expect(md5(data)).toEqual("580c3453b5d102d5b06411c8a1fc4b23");
});
test("Malformed SVG", async () => {
@@ -168,7 +168,7 @@ describe("SVG to image conversion", () => {
width: 187,
height: 150
});
- expect(md5(data)).toEqual("a35bb124b354bb861a6b65118ff16dde");
+ expect(md5(data)).toEqual("1b1fe94386407eecc2d083882aa73589");
});
test("Resize responsive SVG (Squashed)", async () => {
@@ -182,7 +182,7 @@ describe("SVG to image conversion", () => {
width: 300,
height: 100
});
- expect(md5(data)).toEqual("f6571224da1e85780c7dc0ea66b7c95c");
+ expect(md5(data)).toEqual("cf75cb874aad3174a90936c9b6454cf8");
});
test("Resize responsive SVG (Proportionally)", async () => {
@@ -195,7 +195,7 @@ describe("SVG to image conversion", () => {
width: 300,
height: 241
});
- expect(md5(data)).toEqual("1245ca2a1868e5148d0bbeacc0245d25");
+ expect(md5(data)).toEqual("6cd910ca5ebee7366cb6ec51da090b45");
});
test("SVG to PNG shorthand", async () => {
@@ -206,7 +206,7 @@ describe("SVG to image conversion", () => {
width: 187,
height: 150
});
- expect(md5(data)).toEqual("a35bb124b354bb861a6b65118ff16dde");
+ expect(md5(data)).toEqual("1b1fe94386407eecc2d083882aa73589");
});
test("SVG to JPEG shorthand", async () => {
@@ -217,6 +217,7 @@ describe("SVG to image conversion", () => {
width: 187,
height: 150
});
+ fs.writeFileSync(`${outputDir}/da0a53cd944c1fbd56a64684969882cd.jpg`, data as Buffer);
expect(md5(data)).toEqual("da0a53cd944c1fbd56a64684969882cd");
});
@@ -228,7 +229,7 @@ describe("SVG to image conversion", () => {
width: 187,
height: 150
});
- expect(md5(data)).toEqual("b1080b283475987c0d57dd16a9f19288");
+ expect(md5(data)).toEqual("d7e682534f5118c62659afe5b13fe33e");
});
test("Clip the image", async () => {
@@ -246,7 +247,7 @@ describe("SVG to image conversion", () => {
width: 100,
height: 100
});
- expect(md5(data)).toEqual("68c1e882efb0a3ce1791e5a6e6b80bd7");
+ expect(md5(data)).toEqual("29f7cd260d1ceaa8aef7964e5f2f7ae0");
});
test("Wait for browser destruction", async (done) => {
@@ -274,6 +275,42 @@ describe("SVG to image conversion", () => {
done();
}, 1000);
});
+
+ test("Propagates error when cannot connect", async (done) => {
+ const convert = svgToImg.connect({ browserWSEndpoint: "ws://localhost:12345" })
+ try {
+ await convert.from("").toPng();
+ done.fail();
+ } catch (error) {
+ expect(error.message).toContain("ECONNREFUSED");
+ done();
+ }
+ });
+
+ test("Propagates error when cannot connect with subsequent attempts", async (done) => {
+ const convert = svgToImg.connect({ browserWSEndpoint: "ws://localhost:12345" });
+ let errors = 0;
+ for (let i = 0; i < 10; i++) {
+ try {
+ await convert.from("").toPng();
+ done.fail();
+ } catch (error) {
+ expect(error.message).toContain("ECONNREFUSED");
+ errors++;
+ }
+ }
+ expect(errors).toBe(10);
+ done();
+ });
+
+ test("Special character encoding", async (done) => {
+ try {
+ await svgToImg.from("").toPng();
+ done();
+ } catch (error) {
+ done.fail();
+ }
+ });
});
// Kill any remaining Chromium instances
diff --git a/src/typings/index.ts b/src/typings/index.ts
index 654cfd9..cab1d01 100644
--- a/src/typings/index.ts
+++ b/src/typings/index.ts
@@ -19,3 +19,9 @@ export interface IOptions {
export interface IShorthandOptions extends IOptions {
type?: never;
}
+
+export interface IConnectOptions {
+ browserWSEndpoint?: string;
+ browserURL?: string;
+ ignoreHTTPSErrors?: boolean;
+}
diff --git a/tslint.json b/tslint.json
index 4855b4d..c5cc6b1 100644
--- a/tslint.json
+++ b/tslint.json
@@ -14,7 +14,8 @@
"no-consecutive-blank-lines": true,
"no-console": false,
"object-literal-sort-keys": false,
- "ordered-imports": false
+ "ordered-imports": false,
+ "max-classes-per-file": false
},
"rulesDirectory": []
}