-
Notifications
You must be signed in to change notification settings - Fork 80
/
Copy pathindex.ts
293 lines (260 loc) · 10.4 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
import { Readable } from 'stream';
import { Chart as ChartJS, ChartConfiguration, ChartComponentLike } from 'chart.js';
import { createCanvas, registerFont, Image } from 'canvas';
import { freshRequire } from './freshRequire';
import { BackgroundColourPlugin } from './backgroundColourPlugin';
export type ChartJSNodeCanvasPlugins = {
/**
* Global plugins, see https://www.chartjs.org/docs/latest/developers/plugins.html.
*/
readonly modern?: ReadonlyArray<string | ChartComponentLike>;
/**
* This will work for plugins that `require` ChartJS themselves.
*/
readonly requireChartJSLegacy?: ReadonlyArray<string>;
/**
* This should work for any plugin that expects a global Chart variable.
*/
readonly globalVariableLegacy?: ReadonlyArray<string>;
/**
* This will work with plugins that just return a plugin object and do no specific loading themselves.
*/
readonly requireLegacy?: ReadonlyArray<string>;
};
export type ChartCallback = (chartJS: typeof ChartJS) => void | Promise<void>;
export type CanvasType = 'pdf' | 'svg';
export type MimeType = 'image/png' | 'image/jpeg';
// https://github.com/Automattic/node-canvas#non-standard-apis
type Canvas = HTMLCanvasElement & {
toBuffer(callback: (err: Error|null, result: Buffer) => void, mimeType?: string, config?: any): void;
toBuffer(mimeType?: string, config?: any): Buffer;
createPNGStream(config?: any): Readable;
createJPEGStream(config?: any): Readable;
createPDFStream(config?: any): Readable;
};
export interface ChartJSNodeCanvasOptions {
/**
* The width of the charts to render, in pixels.
*/
readonly width: number;
/**
* The height of the charts to render, in pixels.
*/
readonly height: number;
/**
* Optional callback which is called once with a new ChartJS global reference as the only parameter.
*/
readonly chartCallback?: ChartCallback;
/**
* Optional canvas type ('PDF' or 'SVG'), see the [canvas pdf doc](https://github.com/Automattic/node-canvas#pdf-output-support).
*/
readonly type?: CanvasType;
/**
* Optional plugins to register.
*/
readonly plugins?: ChartJSNodeCanvasPlugins;
/**
* Optional background color for the chart, otherwise it will be transparent. Note, this will apply to all charts. See the [fillStyle](https://www.w3schools.com/tags/canvas_fillstyle.asp) canvas API used for possible values.
*/
readonly backgroundColour?: string;
}
export class ChartJSNodeCanvas {
private readonly _width: number;
private readonly _height: number;
private readonly _chartJs: typeof ChartJS;
private readonly _createCanvas: typeof createCanvas;
private readonly _registerFont: typeof registerFont;
private readonly _image: typeof Image;
private readonly _type?: CanvasType;
/**
* Create a new instance of CanvasRenderService.
*
* @param options Configuration for this instance
*/
constructor(options: ChartJSNodeCanvasOptions) {
if (options === null || typeof (options) !== 'object') {
throw new Error('An options parameter object is required');
}
if (!options.width || typeof (options.width) !== 'number') {
throw new Error('A width option is required');
}
if (!options.height || typeof (options.height) !== 'number') {
throw new Error('A height option is required');
}
this._width = options.width;
this._height = options.height;
const canvas = freshRequire('canvas');
this._createCanvas = canvas.createCanvas;
this._registerFont = canvas.registerFont;
this._image = canvas.Image;
this._type = options.type && options.type.toLowerCase() as CanvasType;
this._chartJs = this.initialize(options);
}
/**
* Render to a data url.
* @see https://github.com/Automattic/node-canvas#canvastodataurl
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType The image format, `image/png` or `image/jpeg`.
*/
public renderToDataURL(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<string> {
const chart = this.renderChart(configuration);
return new Promise<string>((resolve, reject) => {
if (!chart.canvas) {
return reject(new Error('Canvas is null'));
}
const canvas = chart.canvas as Canvas;
canvas.toDataURL(mimeType, (error: Error | null, png: string) => {
chart.destroy();
if (error) {
return reject(error);
}
return resolve(png);
});
});
}
/**
* Render to a data url synchronously.
* @see https://github.com/Automattic/node-canvas#canvastodataurl
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType The image format, `image/png` or `image/jpeg`.
*/
public renderToDataURLSync(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): string {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
const dataUrl = canvas.toDataURL(mimeType);
chart.destroy();
return dataUrl;
}
/**
* Render to a buffer.
* @see https://github.com/Automattic/node-canvas#canvastobuffer
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support) or `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToBuffer(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise<Buffer> {
const chart = this.renderChart(configuration);
return new Promise<Buffer>((resolve, reject) => {
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
canvas.toBuffer((error: Error | null, buffer: Buffer) => {
chart.destroy();
if (error) {
return reject(error);
}
return resolve(buffer);
}, mimeType);
});
}
/**
* Render to a buffer synchronously.
* @see https://github.com/Automattic/node-canvas#canvastobuffer
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `raw` (unencoded ARGB32 data in native-endian byte order, top-to-bottom), `application/pdf` (for PDF canvases) and image/svg+xml (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToBufferSync(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' | 'image/svg+xml' = 'image/png'): Buffer {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
const buffer = canvas.toBuffer(mimeType);
chart.destroy();
return buffer;
}
/**
* Render to a stream.
* @see https://github.com/Automattic/node-canvas#canvascreatepngstream
*
* @param configuration The Chart JS configuration for the chart to render.
* @param mimeType A string indicating the image format. Valid options are `image/png`, `image/jpeg` (if node-canvas was built with JPEG support), `application/pdf` (for PDF canvases) and image/svg+xml (for SVG canvases). Defaults to `image/png` for image canvases, or the corresponding type for PDF or SVG canvas.
*/
public renderToStream(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' = 'image/png'): Readable {
const chart = this.renderChart(configuration);
if (!chart.canvas) {
throw new Error('Canvas is null');
}
const canvas = chart.canvas as Canvas;
setImmediate(() => chart.destroy());
switch (mimeType) {
case 'image/png':
return canvas.createPNGStream();
case 'image/jpeg':
return canvas.createJPEGStream();
case 'application/pdf':
return canvas.createPDFStream();
default:
throw new Error(`Un-handled mimeType: ${mimeType}`);
}
}
/**
* Use to register the font with Canvas to use a font file that is not installed as a system font, this must be done before the Canvas is created.
*
* @param path The path to the font file.
* @param options The font options.
* @example
* registerFont('comicsans.ttf', { family: 'Comic Sans' });
*/
public registerFont(path: string, options: { readonly family: string, readonly weight?: string, readonly style?: string }): void {
this._registerFont(path, options);
}
private initialize(options: ChartJSNodeCanvasOptions): typeof ChartJS {
const chartJs: typeof ChartJS = require('chart.js');
if (options.plugins?.requireChartJSLegacy) {
for (const plugin of options.plugins.requireChartJSLegacy) {
require(plugin);
delete require.cache[require.resolve(plugin)];
}
}
if (options.plugins?.globalVariableLegacy) {
(global as any).Chart = chartJs;
for (const plugin of options.plugins.globalVariableLegacy) {
freshRequire(plugin);
}
delete (global as any).Chart;
}
if (options.plugins?.modern) {
for (const plugin of options.plugins.modern) {
if (typeof plugin === 'string') {
chartJs.register(freshRequire(plugin));
} else {
chartJs.register(plugin);
}
}
}
if (options.plugins?.requireLegacy) {
for (const plugin of options.plugins.requireLegacy) {
chartJs.register(freshRequire(plugin));
}
}
if (options.chartCallback) {
options.chartCallback(chartJs);
}
if (options.backgroundColour) {
chartJs.register(new BackgroundColourPlugin(options.width, options.height, options.backgroundColour));
}
delete require.cache[require.resolve('chart.js')];
return chartJs;
}
private renderChart(configuration: ChartConfiguration): ChartJS {
const canvas = this._createCanvas(this._width, this._height, this._type);
(canvas as any).style = (canvas as any).style || {};
// Disable animation (otherwise charts will throw exceptions)
configuration.options = configuration.options || {};
configuration.options.responsive = false;
configuration.options.animation = false as any;
const context = canvas.getContext('2d');
(global as any).Image = this._image; // Some plugins use this API
const chart = new this._chartJs(context, configuration);
delete (global as any).Image;
return chart;
}
}