Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connect to Puppeteer Endpoint, Update Puppeteer to 2.0.0 #19

Closed
wants to merge 12 commits into from
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 set the [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#environment-variables) environment variable.
qqilihq marked this conversation as resolved.
Show resolved Hide resolved

#### Debian

Expand Down Expand Up @@ -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("<svg xmlns='http://www.w3.org/2000/svg'/>").toPng();

console.log(image);
})();
```

## API Documentation

### svgToImg.from(svg)
Expand All @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this portion of the doc is not up-to-date with the actual behavior.

errors will be thrown later when calling one of the to functions

Looking at the tests, it seams the connect method will throw if it can't connect to the websocket endpoint.

https://github.com/etienne-martin/svg-to-img/pull/19/files#diff-4ac74674fef9fbe68755b3bdcb9fbfd4R287

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the test case was not quite accurate (connect should be outside of try). I updated the test code to reflect this. The readme is actually correct.


### 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.
Expand Down Expand Up @@ -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)
qqilihq marked this conversation as resolved.
Show resolved Hide resolved

## License

Expand Down
2 changes: 1 addition & 1 deletion dist/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ 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;
img.src = "data:image/svg+xml;base64," + btoa(svg);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the svg string needs to be base64 encoded because of the websocket endpoint. Should it just work the same regardless if you're connected to a local Chromium instance or a remote browser?

Copy link
Author

@qqilihq qqilihq Dec 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running via WebSocket I get the "Malformed SVG" error triggered in onError.

Admittedly, I have no real clue why this only happens when running remotely (my naive assumption would probably be that the WebSocket connection breaks something with the transmitted SVG string/function?). 🤔

I've added a code comment why the encoding is necessary.

});
};
35 changes: 29 additions & 6 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
/// <reference types="node" />
import * as puppeteer from "puppeteer";
import { IOptions, IShorthandOptions } from "./typings";
export declare const from: (svg: string | Buffer) => {
to: (options: IOptions) => Promise<string | Buffer>;
toPng: (options?: IShorthandOptions | undefined) => Promise<string | Buffer>;
toJpeg: (options?: IShorthandOptions | undefined) => Promise<string | Buffer>;
toWebp: (options?: IShorthandOptions | undefined) => Promise<string | Buffer>;
};
export declare class BrowserSource {
private readonly factory;
private queue;
private browserDestructionTimeout;
private browserInstance;
private browserState;
constructor(factory: () => Promise<puppeteer.Browser>);
getBrowser(): Promise<puppeteer.Browser>;
scheduleBrowserForDestruction(): void;
private executeQueuedRequests;
}
export declare class Svg {
private readonly svg;
private browserSource;
constructor(svg: Buffer | string, browserSource: BrowserSource);
to(options: IOptions): Promise<Buffer | string>;
toPng(options?: IShorthandOptions): Promise<Buffer | string>;
toJpeg(options?: IShorthandOptions): Promise<Buffer | string>;
toWebp(options?: IShorthandOptions): Promise<Buffer | string>;
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?: puppeteer.ConnectOptions | undefined) => SvgToImg;
234 changes: 136 additions & 98 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,113 +3,151 @@ 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);
// tslint:disable-next-line: max-classes-per-file
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;
;
// tslint:disable-next-line: max-classes-per-file
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;
// tslint:disable-next-line: max-classes-per-file
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)));
};
Loading