├── .nvmrc ├── .npmrc ├── dist ├── freshRequire.d.ts ├── index.d.ts ├── index.js.map ├── index.js ├── freshRequire.js.map └── freshRequire.js ├── codealike.json ├── resources ├── logo.png ├── logo.xcf ├── example.gif ├── example.png ├── memory-usage-same-instance.png ├── memory-usage-new-instance-parallel.png ├── memory-usage-new-instance-sequential.png ├── chartjs-logo.svg └── nodejs-logo.svg ├── testData ├── Anthrope.ttf ├── VTKS UNAMOUR.ttf ├── linux │ ├── font.png │ ├── render-to-buffer-diff.png │ ├── render-to-stream-diff.png │ ├── render-to-buffer-actual.png │ ├── render-to-data-URL-diff.txt │ ├── render-to-stream-actual.png │ ├── chartjs-plugin-annotation.png │ ├── chartjs-plugin-datalabels.png │ ├── no-background-color-actual.png │ ├── no-background-color-diff.png │ ├── chartjs-plugin-annotation-diff.png │ ├── chartjs-plugin-datalabels-diff.png │ ├── chartjs-plugin-annotation-actual.png │ ├── chartjs-plugin-datalabels-actual.png │ ├── render-to-data-URL-actual.txt │ └── render-to-data-URL-compare.html ├── linuxw │ ├── font.png │ ├── background-color.png │ ├── render-to-buffer.png │ ├── render-to-stream.png │ ├── no-background-color.png │ ├── chartjs-plugin-annotation.png │ ├── chartjs-plugin-datalabels.png │ └── render-to-data-URL.txt └── win32 │ ├── font.png │ ├── background-color.png │ ├── render-to-buffer.png │ ├── render-to-stream.png │ ├── no-background-color.png │ ├── chartjs-plugin-annotation.png │ ├── chartjs-plugin-datalabels.png │ └── render-to-data-URL.txt ├── src ├── index.ts ├── global.d.ts ├── freshRequire.ts ├── backgroundColourPlugin.ts ├── freshImport.ts ├── example.ts ├── animatedChartJSNodeCanvas.ts ├── chartJSNodeCanvas.ts ├── chartJSNodeCanvasBase.ts ├── index.spec.ts └── index.e2e.spec.ts ├── tests └── require │ └── wtfnode.js ├── docker ├── production │ └── Dockerfile ├── build │ └── Dockerfile └── base │ └── Dockerfile ├── .c8rc ├── .editorconfig ├── .npmignore ├── scripts ├── publish.sh ├── clean-dest.js └── package-size.js ├── docker-compose.override.yml ├── .dockerignore ├── .mocharc.js ├── CHANGELOG.md ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── docker-compose.yml ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── nodejs.yml ├── tsconfig.json ├── .gitignore ├── package.json ├── tslint.json ├── API.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 21.7.3 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | msvs_version = 2022 -------------------------------------------------------------------------------- /dist/freshRequire.d.ts: -------------------------------------------------------------------------------- 1 | export declare const freshRequire: (id: string) => any; 2 | -------------------------------------------------------------------------------- /codealike.json: -------------------------------------------------------------------------------- 1 | {"projectId":"edd7e570-506c-11e9-af99-077b651e3121","projectName":"chartjs-node-canvas"} -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/logo.png -------------------------------------------------------------------------------- /resources/logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/logo.xcf -------------------------------------------------------------------------------- /resources/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/example.gif -------------------------------------------------------------------------------- /resources/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/example.png -------------------------------------------------------------------------------- /testData/Anthrope.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/Anthrope.ttf -------------------------------------------------------------------------------- /testData/VTKS UNAMOUR.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/VTKS UNAMOUR.ttf -------------------------------------------------------------------------------- /testData/linux/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/font.png -------------------------------------------------------------------------------- /testData/linuxw/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/font.png -------------------------------------------------------------------------------- /testData/win32/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/font.png -------------------------------------------------------------------------------- /testData/win32/background-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/background-color.png -------------------------------------------------------------------------------- /testData/win32/render-to-buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/render-to-buffer.png -------------------------------------------------------------------------------- /testData/win32/render-to-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/render-to-stream.png -------------------------------------------------------------------------------- /testData/linuxw/background-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/background-color.png -------------------------------------------------------------------------------- /testData/linuxw/render-to-buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/render-to-buffer.png -------------------------------------------------------------------------------- /testData/linuxw/render-to-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/render-to-stream.png -------------------------------------------------------------------------------- /resources/memory-usage-same-instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/memory-usage-same-instance.png -------------------------------------------------------------------------------- /testData/linux/render-to-buffer-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/render-to-buffer-diff.png -------------------------------------------------------------------------------- /testData/linux/render-to-stream-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/render-to-stream-diff.png -------------------------------------------------------------------------------- /testData/linuxw/no-background-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/no-background-color.png -------------------------------------------------------------------------------- /testData/win32/no-background-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/no-background-color.png -------------------------------------------------------------------------------- /testData/linux/render-to-buffer-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/render-to-buffer-actual.png -------------------------------------------------------------------------------- /testData/linux/render-to-data-URL-diff.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/render-to-data-URL-diff.txt -------------------------------------------------------------------------------- /testData/linux/render-to-stream-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/render-to-stream-actual.png -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './animatedChartJSNodeCanvas'; 2 | export * from './chartJSNodeCanvas'; 3 | export * from './chartJSNodeCanvasBase'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './animatedChartJSNodeCanvas'; 2 | export * from './chartJSNodeCanvas'; 3 | export * from './chartJSNodeCanvasBase'; 4 | -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-annotation.png -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-datalabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-datalabels.png -------------------------------------------------------------------------------- /testData/linux/no-background-color-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/no-background-color-actual.png -------------------------------------------------------------------------------- /testData/linux/no-background-color-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/no-background-color-diff.png -------------------------------------------------------------------------------- /testData/linuxw/chartjs-plugin-annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/chartjs-plugin-annotation.png -------------------------------------------------------------------------------- /testData/linuxw/chartjs-plugin-datalabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linuxw/chartjs-plugin-datalabels.png -------------------------------------------------------------------------------- /testData/win32/chartjs-plugin-annotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/chartjs-plugin-annotation.png -------------------------------------------------------------------------------- /testData/win32/chartjs-plugin-datalabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/win32/chartjs-plugin-datalabels.png -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,sEAA4C;AAC5C,8DAAoC;AACpC,kEAAwC"} -------------------------------------------------------------------------------- /resources/memory-usage-new-instance-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/memory-usage-new-instance-parallel.png -------------------------------------------------------------------------------- /resources/memory-usage-new-instance-sequential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/resources/memory-usage-new-instance-sequential.png -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-annotation-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-annotation-diff.png -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-datalabels-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-datalabels-diff.png -------------------------------------------------------------------------------- /tests/require/wtfnode.js: -------------------------------------------------------------------------------- 1 | const wtfnode = require('wtfnode'); 2 | const { after } = require('mocha'); 3 | 4 | after(() => { 5 | wtfnode.dump(); 6 | }); 7 | -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-annotation-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-annotation-actual.png -------------------------------------------------------------------------------- /testData/linux/chartjs-plugin-datalabels-actual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanSobey/ChartjsNodeCanvas/HEAD/testData/linux/chartjs-plugin-datalabels-actual.png -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | // syntax: 2 | // declare module ''; 3 | 4 | declare module 'resemblejs'; // Has a 'declare global' that causes typescript to add a '/// ' 5 | -------------------------------------------------------------------------------- /docker/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM chartjs-node-canvas-base 2 | 3 | LABEL maintainer = "sean.m.sobey@gmail.com" 4 | 5 | WORKDIR /usr/server 6 | 7 | COPY --from=chartjs-node-canvas-build /usr/server/dist/ ./dist/ 8 | 9 | CMD ["/bin/bash", "-c", "echo production image complete"] -------------------------------------------------------------------------------- /docker/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM chartjs-node-canvas-base 2 | 3 | LABEL maintainer = "sean.m.sobey@gmail.com" 4 | 5 | WORKDIR /usr/server 6 | 7 | RUN npm ci --no-color 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | CMD ["/bin/bash", "-c", "echo base image complete"] -------------------------------------------------------------------------------- /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "cobertura", 5 | "text-summary", 6 | "json" 7 | ], 8 | "exclude": [ 9 | "dist/**/*.spec.js", 10 | "dist/example.js" 11 | ], 12 | "include": [ 13 | "dist/**/*.js" 14 | ], 15 | "temp-directory": "./coverage", 16 | "reports-dir": "./reports/test-coverage" 17 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{ts,js,css,html}] 2 | indent_style = tab 3 | tab_width = 4 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.{ts,js}] 9 | quote_type = single 10 | 11 | [*.{json,yaml,yml}] 12 | indent_style = space 13 | tab_width = 2 14 | 15 | [*.md] 16 | indent_style = space 17 | tab_width = 4 -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const tslib_1 = require("tslib"); 4 | tslib_1.__exportStar(require("./animatedChartJSNodeCanvas"), exports); 5 | tslib_1.__exportStar(require("./chartJSNodeCanvas"), exports); 6 | tslib_1.__exportStar(require("./chartJSNodeCanvasBase"), exports); 7 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /src/freshRequire.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/hughsk/fresh-require 2 | 3 | export const freshRequire: /*NodeJS.Require*/ (id: string) => any = (file) => { 4 | 5 | const resolvedFile = require.resolve(file); 6 | const temp = require.cache[resolvedFile]; 7 | delete require.cache[resolvedFile]; 8 | const modified = require(resolvedFile); 9 | require.cache[resolvedFile] = temp; 10 | return modified; 11 | }; 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/**/* 2 | docker/ 3 | .circleci/ 4 | .github/ 5 | reports/ 6 | scripts/ 7 | tests/ 8 | testData/ 9 | coverage/ 10 | resources/ 11 | 12 | .dockerignore 13 | docker-compose.override.yml 14 | docker-compose.yml 15 | .editorconfig 16 | codealike.json 17 | .mocharc.json 18 | tsconfig.json 19 | tslint.json 20 | .c8rc 21 | .nvmrc 22 | .tsbuildinfo 23 | .mocharc.js 24 | 25 | src/**/*.spec.* 26 | !dist/** 27 | dist/**/*.spec.* -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | packageName=$(node -p "require('./package.json').name") 4 | packageVersion=$(node -p "require('./package.json').version") 5 | publishVersion=$(npm view "$packageName" version) 6 | if [ "$packageVersion" != "$publishVersion" ] 7 | then 8 | npm config set //registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN 9 | npm publish 10 | else 11 | echo "Existing version $packageVersion for $packageName is already published...skipping" 12 | fi -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | # Override default compose file here. 4 | # https://docs.docker.com/compose/extends/#example-use-case 5 | 6 | 7 | # ci can skip copying the override file or delete it. 8 | # or ci can run command 9 | # docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d 10 | 11 | # services: 12 | # test: 13 | # command: /bin/bash -c echo skipping tests 14 | # production: 15 | # depends_on: 16 | # - build -------------------------------------------------------------------------------- /dist/freshRequire.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"freshRequire.js","sourceRoot":"","sources":["../src/freshRequire.ts"],"names":[],"mappings":";AAAA,0CAA0C;;;AAEnC,MAAM,YAAY,GAA2C,CAAC,IAAI,EAAE,EAAE;IAE5E,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACzC,OAAO,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC;IACnC,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AARW,QAAA,YAAY,gBAQvB"} -------------------------------------------------------------------------------- /scripts/clean-dest.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 'use-strict'; 3 | 4 | const path = require('path'); 5 | 6 | module.exports = exports = { 7 | '.ts': (destFilePath) => { 8 | const dirname = path.dirname(destFilePath); 9 | const basename = path.basename(destFilePath, path.extname(destFilePath)); 10 | return [ 11 | path.posix.join(dirname, basename + '.d.ts'), 12 | path.posix.join(dirname, basename + '.js'), 13 | path.posix.join(dirname, basename + '.js.map'), 14 | ] 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /docker/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21.7.3-slim 2 | 3 | LABEL maintainer = "sean.m.sobey@gmail.com" 4 | 5 | RUN apt-get update && apt-get install -y --no-install-recommends \ 6 | # - For node-gyp 7 | # python2 make g++ \ 8 | # - For canvas 9 | fontconfig build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev 10 | 11 | WORKDIR /usr/server 12 | 13 | COPY package.json package-lock.json ./ 14 | 15 | RUN npm ci --production --no-color 16 | 17 | CMD ["/bin/bash", "-c", "echo base image complete"] -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | **/node_modules/** 8 | 9 | # Git 10 | .git/ 11 | .gitignore 12 | .gitattributes 13 | 14 | # VSCode 15 | .vscode/ 16 | 17 | #dist 18 | dist/ 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | #docker 30 | docker/ 31 | docker-compose.yml 32 | docker-compose.override.yml -------------------------------------------------------------------------------- /dist/freshRequire.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // https://github.com/hughsk/fresh-require 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.freshRequire = void 0; 5 | const freshRequire = (file) => { 6 | const resolvedFile = require.resolve(file); 7 | const temp = require.cache[resolvedFile]; 8 | delete require.cache[resolvedFile]; 9 | const modified = require(resolvedFile); 10 | require.cache[resolvedFile] = temp; 11 | return modified; 12 | }; 13 | exports.freshRequire = freshRequire; 14 | //# sourceMappingURL=freshRequire.js.map -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 'use strict'; 3 | 4 | // https://mochajs.org/#command-line-usage 5 | 6 | const NODE_ENV = process.env.NODE_ENV; // 'production' | 'ci' | 'test' | 'debug' 7 | 8 | /**@type {import('mocha').MochaOptions}*/ 9 | module.exports = { 10 | 'ignore-leaks': false, 11 | 'allow-uncaught': true, 12 | 'globals': [ 13 | 'window' 14 | ], 15 | 'require': [ 16 | 'source-map-support/register', 17 | ], 18 | 'timeout': 50000, 19 | 'file': NODE_ENV === 'debug' 20 | ? [] 21 | : [ 22 | './tests/require/wtfnode.js' 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.0 4 | 5 | Migrated to support chart.js v4.x.x, dropped support for 3.x.x 6 | Upgraded to canvas v3.x.x 7 | 8 | WIP support for animated charts, exporting to GIF. 9 | 10 | ## 4.0.0 11 | 12 | Migrated to support chart.js v3.x.x, dropped support for 2.x.x 13 | Removed the legacy API. 14 | 15 | ## 3.2.0 16 | 17 | Another deploy for minor versions. 18 | 19 | ## 3.1.1 20 | 21 | Added back legacy API to address a wrong version being deployed. See [#60](https://github.com/SeanSobey/ChartjsNodeCanvas/issues/60). 22 | 23 | ## 3.0.0 24 | 25 | Rewrote `ChartJSNodeCanvas` API, the constructor and plugins in particular. 26 | -------------------------------------------------------------------------------- /src/backgroundColourPlugin.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error moduleResolution:nodenext issue 54523 2 | import { Chart as ChartJS, Plugin as ChartJSPlugin } from 'chart.js/auto'; 3 | 4 | export class BackgroundColourPlugin implements ChartJSPlugin { 5 | public readonly id: string = 'chartjs-plugin-chartjs-node-canvas-background-colour'; 6 | 7 | public constructor( 8 | private readonly _width: number, 9 | private readonly _height: number, 10 | private readonly _fillStyle: string 11 | ) { } 12 | 13 | public beforeDraw(chart: ChartJS): boolean | void { 14 | 15 | const ctx = chart.ctx; 16 | ctx.save(); 17 | ctx.globalCompositeOperation = 'destination-over'; 18 | ctx.fillStyle = this._fillStyle; 19 | ctx.fillRect(0, 0, this._width, this._height); 20 | ctx.restore(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/freshImport.ts: -------------------------------------------------------------------------------- 1 | // ES Module version of fresh-require 2 | // export const freshImport = async (modulePath: string): Promise => { 3 | // // For ES modules, we need to construct the full URL 4 | // const moduleUrl = new URL(modulePath, import.meta.url).href; 5 | 6 | // // Clear module from import cache if possible 7 | // // Note: ES modules caching works differently than CommonJS 8 | // try { 9 | // // Force a new module instance by appending a cache-busting query parameter 10 | // const timestamp = Date.now(); 11 | // const urlWithCacheBuster = `${moduleUrl}?cache=${timestamp}`; 12 | 13 | // // Dynamic import with cache buster 14 | // const module = await import(/* @vite-ignore */ urlWithCacheBuster); 15 | // return module; 16 | // } catch (error) { 17 | // console.error(`Error importing module ${modulePath}:`, error); 18 | // throw error; 19 | // } 20 | // }; 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceRoot}", 12 | "runtimeExecutable": "npm", 13 | "windows": { 14 | "runtimeExecutable": "npm.cmd" 15 | }, 16 | "runtimeArgs": [ 17 | "run-script", 18 | "debug-test" 19 | ], 20 | "preLaunchTask": "nvm", 21 | "timeout": 50000, 22 | "port": 33295, 23 | "sourceMaps": true, 24 | "outFiles": [ 25 | "${workspaceRoot}/dist/**/*.js" 26 | ], 27 | "skipFiles": [ 28 | "/**" 29 | ], 30 | "env": { 31 | "NODE_ENV": "debug" 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative", 3 | "typescript.preferences.quoteStyle": "single", 4 | "files.associations": { 5 | ".c8rc": "json", 6 | ".dockerignore": "gitignore", 7 | ".env.*": "dotenv", 8 | ".nycrc": "json", 9 | "Dockerfile.*": "dockerfile" 10 | }, 11 | "eslint.validate": [ 12 | "javascript", 13 | "javascriptreact", 14 | "typescript", 15 | "typescriptreact", 16 | ], 17 | "mochaExplorer.debuggerConfig": "Launch Tests", 18 | "mochaExplorer.files": "dist/**/*.js", 19 | "mochaExplorer.require": "source-map-support/register", 20 | "mocha.enabled": true, 21 | "mocha.options": { 22 | "checkLeaks": true, 23 | "throwDeprecation": true, 24 | "traceDeprecation": true, 25 | "traceWarnings": true, 26 | "allowUncaught": true 27 | }, 28 | "mocha.sourceDir": "src/", 29 | "mocha.outputDir": "dist/", 30 | "mocha.glob": "**/*.spec.js", 31 | "mocha.compilerScript": "build", 32 | "mocha.requires": [ 33 | "source-map-support/register" 34 | ], 35 | "deepcode.review.results.hideInformationIssues": false 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sean Sobey 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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | base: 5 | image: chartjs-node-canvas-base 6 | build: 7 | context: . 8 | dockerfile: docker/base/Dockerfile 9 | container_name: chartjs-node-canvas-base 10 | build: 11 | image: chartjs-node-canvas-build 12 | depends_on: 13 | - base 14 | build: 15 | context: . 16 | dockerfile: docker/build/Dockerfile 17 | container_name: chartjs-node-canvas-build 18 | test: 19 | image: chartjs-node-canvas-build 20 | container_name: chartjs-node-canvas-test 21 | depends_on: 22 | - build 23 | environment: 24 | - NODE_ENV=test 25 | - FONTCONFIG_PATH=/etc/fonts 26 | command: npm run test 27 | production: 28 | image: chartjs-node-canvas 29 | depends_on: 30 | - test 31 | build: 32 | context: . 33 | dockerfile: docker/production/Dockerfile 34 | container_name: chartjs-node-canvas 35 | environment: 36 | - NODE_ENV=production 37 | 38 | #<< Docker Commands >> 39 | 40 | # Build Images 41 | #docker-compose build 42 | 43 | # Run Containers 44 | #docker-compose up 45 | 46 | # Run Tests specifically 47 | #docker-compose up test 48 | -------------------------------------------------------------------------------- /resources/chartjs-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Troubleshooting Checklist** 11 | * You have a supported [Node JS version](https://github.com/SeanSobey/ChartjsNodeCanvas#node-js-version). 12 | * You have a supported [Chart.JS version](https://github.com/SeanSobey/ChartjsNodeCanvas#chartsjs-version). 13 | * If this is plugin related: 14 | - You have read the [plugin docs](https://github.com/SeanSobey/ChartjsNodeCanvas#loading-plugins). 15 | - You have read the chart.js [plugin docs](https://www.chartjs.org/docs/latest/developers/plugins.html). 16 | - You have searched for [similar plugin issues](https://github.com/SeanSobey/ChartjsNodeCanvas/issues?q=is%3Aissue+label%3Aplugin). 17 | * If this is deployment/environment related: 18 | - You have read the [canvas docs](https://github.com/Automattic/node-canvas) and followed any environment specific steps. 19 | - You have searched for [similar deployment issues](https://github.com/SeanSobey/ChartjsNodeCanvas/issues?q=is%3Aissue+label%3Adeployment). 20 | 21 | 22 | **Describe the bug** 23 | A clear and concise description of what the bug is. 24 | 25 | **Versions** 26 | * NodeJS version: x.x.x 27 | * Chart.JS version: x.x.x 28 | * Typescript version (if applicable): x.x.x 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", 5 | "module": "Node16", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "outDir": "./dist/", 9 | "importHelpers": true, 10 | "incremental": true, 11 | "tsBuildInfoFile": "./.tsbuildinfo", 12 | "noEmitOnError": false, 13 | 14 | /* Strict Type-Checking Options */ 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "strictPropertyInitialization": true, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | 23 | /* Additional Checks */ 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": true, 26 | "noImplicitReturns": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "skipLibCheck": true, 29 | 30 | /* Module Resolution Options */ 31 | "moduleResolution": "node16", 32 | "baseUrl": "./", 33 | "allowSyntheticDefaultImports": true, 34 | "esModuleInterop": true, 35 | "resolveJsonModule": true, 36 | "typeRoots": [ 37 | "./node_modules/@types", 38 | "./node_modules/chart.js/auto" 39 | ], 40 | 41 | /* Source Map Options */ 42 | "inlineSourceMap": false, 43 | "inlineSources": false, 44 | 45 | /* Experimental Options */ 46 | "experimentalDecorators": true, 47 | "emitDecoratorMetadata": true, 48 | "pretty": true 49 | }, 50 | "include": [ 51 | "./src/**/*" 52 | ], 53 | "buildOptions": { 54 | "verbose": true 55 | } 56 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "shell", 8 | "windows": { 9 | "command": "nvm use $(Get-Content .nvmrc)", 10 | }, 11 | "linux":{ 12 | "command": "cat .nvmrc | nvm use" 13 | }, 14 | "osx":{ 15 | "command": "cat .nvmrc | nvm use" 16 | }, 17 | "label": "nvm", 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "build", 22 | "dependsOn": "nvm", 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "problemMatcher": "$tsc" 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "test", 32 | "dependsOn": "nvm", 33 | "group": { 34 | "kind": "test", 35 | "isDefault": true 36 | } 37 | }, 38 | { 39 | "type": "npm", 40 | "script": "clean", 41 | "dependsOn": "nvm" 42 | }, 43 | { 44 | "type": "npm", 45 | "script": "lint", 46 | "dependsOn": "nvm", 47 | "problemMatcher": "$eslint-stylish" 48 | }, 49 | { 50 | "type": "npm", 51 | "script": "docs" 52 | }, 53 | { 54 | "type": "npm", 55 | "script": "watch-build", 56 | "dependsOn": "nvm", 57 | "group": "build", 58 | "isBackground": true, 59 | "problemMatcher": "$tsc" 60 | }, 61 | { 62 | "type": "npm", 63 | "script": "watch-clean", 64 | "isBackground": true 65 | }, 66 | { 67 | "type": "npm", 68 | "script": "watch-test", 69 | "dependsOn": "nvm", 70 | "isBackground": true, 71 | "group": "test" 72 | }, 73 | { 74 | "type": "npm", 75 | "script": "watch-lint", 76 | "dependsOn": "nvm", 77 | "isBackground": true, 78 | "problemMatcher": "$eslint-stylish" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /scripts/package-size.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 'use-strict'; 3 | 4 | // const yargs = require('yargs'); 5 | const { exec } = require('child_process'); 6 | const { EOL } = require('os'); 7 | const { promisify } = require('util'); 8 | 9 | const execAsync = promisify(exec); 10 | 11 | // const argv = yargs.options({}) 12 | // .strict() 13 | // .config() 14 | // .argv; 15 | 16 | async function main() { 17 | 18 | const maxIncreaseDelta = 0.05; 19 | const packageInfo = require('../package.json'); 20 | const package = await dryRunPack(); 21 | const publishedPackage = await dryRunPack(packageInfo.name); 22 | const increaseDelta = (package.size - publishedPackage.size) / publishedPackage.size; 23 | if (increaseDelta > maxIncreaseDelta) { 24 | const packageContents = package.files.map(file => file.path); 25 | throw new Error(`Package increased in size by ${Math.round(increaseDelta * 100)}%, (${package.size - publishedPackage.size} bytes)${EOL}== Tarball Contents ===${EOL}${packageContents.join(EOL)}`); 26 | } 27 | console.log('Package size is within delta threshold', { publishedPackageSize: publishedPackage.size, packageSize: package.size, increaseDelta }); 28 | } 29 | 30 | /** 31 | * @param {string} name 32 | * @param {string} version 33 | * @returns {Promise} 34 | */ 35 | async function dryRunPack(name = null, version = null) { 36 | 37 | // https://docs.npmjs.com/cli-commands/pack.html 38 | const package = name 39 | ? `${name}@${version || 'latest'}` 40 | : ''; 41 | const { stderr, stdout } = await execAsync(`npm pack ${package} --dry-run --json`); 42 | try { 43 | const data = JSON.parse(stdout); 44 | if (!data || !Array.isArray(data)) { 45 | throw new Error(`Bad json data: ${EOL}${data}${EOL}json: ${EOL}${stdout}`); 46 | } 47 | if (data.length !== 1) { 48 | throw new Error(`Bad array data: ${EOL}${data}${EOL}`); 49 | } 50 | return data[0]; 51 | } catch (error) { 52 | if (error instanceof SyntaxError) { 53 | throw new Error(`Failed to parse json with error: ${EOL}${error}${EOL}json: ${EOL}${stdout}`) 54 | } 55 | throw error; 56 | } 57 | } 58 | 59 | main() 60 | .catch((error) => console.error(error)); 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | # reports 106 | /reports/** 107 | !/reports/index.html 108 | 109 | #dist 110 | dist/** 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chartjs-node-canvas", 3 | "version": "5.0.0", 4 | "description": "A node renderer for Chart.js using canvas.", 5 | "main": "./dist/index", 6 | "type": "commonjs", 7 | "types": "./dist/index.d.ts", 8 | "scripts": { 9 | "nvm": "cat .nvmrc | nvm use", 10 | "build": "tsc", 11 | "clean": "clean-dest -s ./src -d ./dist --file-map ./scripts/clean-dest --permanent --verbose && echo TODO: Delete .tsbuildinfo?", 12 | "lint": "echo TODO: Add linting", 13 | "test": "c8 --all mocha dist/**/*.spec.js", 14 | "test-unit": "mocha --exclude dist/**/*.e2e.spec.js dist/**/*.spec.js", 15 | "test-e2e": "mocha dist/**/*.e2e.spec.js", 16 | "package-size": "node ./scripts/package-size", 17 | "debug-test": "node --inspect-brk=33295 --nolazy node_modules/mocha/bin/_mocha dist/**/*.spec.js", 18 | "watch-build": "tsc --watch", 19 | "watch-clean": "nodemon --watch ./src -e ts --exec npm run-script clean", 20 | "watch-test": "nodemon --watch ./dist -e js --exec npm run-script test", 21 | "docs": "jsdoc2md dist/index.js > API.md" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/SeanSobey/ChartjsNodeCanvas/issues", 25 | "email": "sean.m.sobey@gmail.com" 26 | }, 27 | "author": { 28 | "name": "Sean Sobey", 29 | "email": "sean.m.sobey@gmail.com" 30 | }, 31 | "homepage": "https://github.com/SeanSobey/ChartjsNodeCanvas", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/SeanSobey/ChartjsNodeCanvas.git" 35 | }, 36 | "readme": "README.md", 37 | "license": "MIT", 38 | "dependencies": { 39 | "canvas": "^3.1.0", 40 | "tslib": "^2.8.1" 41 | }, 42 | "peerDependencies": { 43 | "chart.js": "^4.4.8" 44 | }, 45 | "devDependencies": { 46 | "@types/mocha": "^7.0.2", 47 | "@types/node": "^16.10.4", 48 | "@types/offscreencanvas": "^2019.7.3", 49 | "c8": "^7.10.0", 50 | "chart.js": "^4.4.8", 51 | "chartjs-plugin-annotation": "^3.1.0", 52 | "chartjs-plugin-crosshair": "^2.0.0", 53 | "chartjs-plugin-datalabels": "^2.2.0", 54 | "clean-dest": "^1.3.3", 55 | "gifencoder": "^2.0.1", 56 | "jsdoc-to-markdown": "^5.0.3", 57 | "mocha": "^7.2.0", 58 | "nodemon": "^2.0.13", 59 | "release-it": "^14.11.6", 60 | "resemblejs": "^4.0.0", 61 | "source-map-support": "^0.5.20", 62 | "ts-std-lib": "^1.2.2", 63 | "typescript": "^5.7.3", 64 | "wtfnode": "^0.9.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/example.ts: -------------------------------------------------------------------------------- 1 | import { ChartJSNodeCanvas, ChartCallback } from './'; 2 | // @ts-expect-error moduleResolution:nodenext issue 54523 3 | import { ChartConfiguration } from 'chart.js/auto'; 4 | import path from 'path'; 5 | import { promises as fs } from 'fs'; 6 | 7 | async function main(): Promise { 8 | 9 | const width = 400; 10 | const height = 400; 11 | const configuration: ChartConfiguration = { 12 | type: 'bar', 13 | data: { 14 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 15 | datasets: [{ 16 | label: '# of Votes', 17 | data: [12, 19, 3, 5, 2, 3], 18 | backgroundColor: [ 19 | 'rgba(255, 99, 132, 0.2)', 20 | 'rgba(54, 162, 235, 0.2)', 21 | 'rgba(255, 206, 86, 0.2)', 22 | 'rgba(75, 192, 192, 0.2)', 23 | 'rgba(153, 102, 255, 0.2)', 24 | 'rgba(255, 159, 64, 0.2)' 25 | ], 26 | borderColor: [ 27 | 'rgba(255,99,132,1)', 28 | 'rgba(54, 162, 235, 1)', 29 | 'rgba(255, 206, 86, 1)', 30 | 'rgba(75, 192, 192, 1)', 31 | 'rgba(153, 102, 255, 1)', 32 | 'rgba(255, 159, 64, 1)' 33 | ], 34 | borderWidth: 1 35 | }] 36 | }, 37 | options: { 38 | }, 39 | plugins: [{ 40 | id: 'background-colour', 41 | beforeDraw: (chart) => { 42 | const ctx = chart.ctx; 43 | ctx.save(); 44 | ctx.fillStyle = 'white'; 45 | ctx.fillRect(0, 0, width, height); 46 | ctx.restore(); 47 | } 48 | }] 49 | }; 50 | const chartCallback: ChartCallback = (ChartJS) => { 51 | ChartJS.defaults.responsive = true; 52 | ChartJS.defaults.maintainAspectRatio = false; 53 | }; 54 | console.log('here'); 55 | 56 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback }); 57 | const buffer = await chartJSNodeCanvas.renderToBuffer(configuration); 58 | await fs.writeFile('./resources/example.png', buffer, 'base64'); 59 | 60 | const k = Object.keys(require.cache).find(key => key.includes(path.join('node_modules','chart.js'))); 61 | console.log('keys', k); 62 | 63 | // const animatedChartJSNodeCanvas = new AnimatedChartJSNodeCanvas({ width, height, chartCallback }); 64 | // const buffers = await animatedChartJSNodeCanvas.renderToBuffer(configuration); 65 | // const { Gif } = await import('make-a-gif'); 66 | // const gif = new Gif(width, height, 1); 67 | // const totalDuration = 1000; 68 | // const duration = totalDuration / buffers.length; 69 | // await gif.setFrames(buffers.map(buffer => ({ src: new Uint8Array(buffer), duration }))); 70 | // // const data = await chartJSNodeCanvas.renderToDataURL(configuration); 71 | // // console.log(data.length); 72 | // const image = await gif.encode(); 73 | // await fs.writeFile('./resources/example.gif', image); 74 | } 75 | main(); 76 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-and-test: 13 | runs-on: windows-latest 14 | strategy: 15 | matrix: 16 | node-version: ['20.x', '21.x'] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install Cairo dependencies 24 | run: | 25 | Invoke-WebRequest "http://ftp.gnome.org/pub/GNOME/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" 26 | Expand-Archive gtk.zip -DestinationPath "C:\GTK" 27 | Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost 28 | .\libjpeg.exe /S 29 | - name: Install Anthrope font 30 | run: | 31 | Copy-Item -Path "${{ github.workspace }}\\testData\\Anthrope.ttf" -Destination "$Env:WINDIR\Fonts" 32 | reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Anthrope (TrueType)" /t REG_SZ /d "Anthrope.ttf" /f 33 | - name: Add msbuild to PATH 34 | uses: microsoft/setup-msbuild@v1.1 35 | with: 36 | vs-version: '15.0' 37 | - name: NPM install 38 | run: npm ci 39 | - name: Build 40 | run: npm run-script build 41 | - name: Lint 42 | run: npm run-script lint 43 | - name: Test 44 | run: npm run-script test 45 | - uses: codecov/codecov-action@v1 46 | with: 47 | name: Node v${{ matrix.node-version }} 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | file: ./reports/test-coverage/cobertura-coverage.xml 50 | fail_ci_if_error: true 51 | # Does not yet support wildcards! Needs v2: https://github.com/actions/upload-artifact/pull/54 52 | # When ready see https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts 53 | # - name: Package Artifact 54 | # run: npm pack 55 | # - name: Upload Artifact 56 | # uses: actions/upload-artifact@v1 57 | # with: 58 | # name: Node v${{ matrix.node-version }} 59 | # path: '*.tgz' 60 | publish: 61 | if: github.ref == 'refs/heads/master' 62 | needs: build-and-test 63 | runs-on: windows-latest 64 | strategy: 65 | matrix: 66 | node-version: ['21.x'] 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Use Node.js ${{ matrix.node-version }} 70 | uses: actions/setup-node@v2 71 | with: 72 | node-version: ${{ matrix.node-version }} 73 | - name: NPM install 74 | run: npm ci 75 | - name: Build 76 | run: npm run-script build 77 | - name: Check Size 78 | run: npm run-script package-size 79 | - name: Publish 80 | shell: bash 81 | run: ./scripts/publish.sh 82 | env: 83 | CI: true 84 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-eslint-rules", 5 | "tslint-immutable", 6 | "tslint-divid" 7 | ], 8 | "rules": { 9 | "arrow-parens": false, 10 | "array-type": [ 11 | true, 12 | "generic" 13 | ], 14 | "ban": [ 15 | true, 16 | [ 17 | "_", 18 | "extend" 19 | ], 20 | [ 21 | "_", 22 | "isNull" 23 | ], 24 | [ 25 | "_", 26 | "isDefined" 27 | ] 28 | ], 29 | "class-name": true, 30 | "comment-format": [ 31 | false 32 | ], 33 | "curly": true, 34 | "eofline": false, 35 | "forin": true, 36 | "indent": [ 37 | true, 38 | "tabs", 39 | 4 40 | ], 41 | "interface-name": [ 42 | false 43 | ], 44 | "interface-over-type-literal": false, 45 | "jsdoc-format": true, 46 | "label-position": true, 47 | "max-line-length": [ 48 | false 49 | ], 50 | "max-classes-per-file": [ 51 | false 52 | ], 53 | "no-arg": true, 54 | "no-bitwise": true, 55 | "no-console": [ 56 | true, 57 | "debug", 58 | "info", 59 | "time", 60 | "timeEnd", 61 | "trace" 62 | ], 63 | "no-construct": true, 64 | "no-debugger": true, 65 | "no-duplicate-variable": true, 66 | "no-empty": true, 67 | "no-empty-interface": false, 68 | "no-eval": true, 69 | "no-inferrable-types": [ 70 | true, 71 | "ignore-params" 72 | ], 73 | "no-string-literal": true, 74 | "no-trailing-whitespace": true, 75 | "no-unused-expression": true, 76 | "no-unused-variable": true, 77 | "no-use-before-declare": false, 78 | "no-var-requires": false, 79 | "object-literal-sort-keys": false, 80 | "one-line": [ 81 | true, 82 | "check-open-brace", 83 | "check-catch", 84 | "check-else", 85 | "check-whitespace" 86 | ], 87 | "ordered-imports": [ 88 | false 89 | ], 90 | "quotemark": [ 91 | true, 92 | "single" 93 | ], 94 | "radix": true, 95 | "semicolon": [ 96 | true, 97 | "always" 98 | ], 99 | "trailing-comma": [ 100 | false 101 | ], 102 | "triple-equals": [ 103 | true, 104 | "allow-null-check" 105 | ], 106 | "typedef": [ 107 | true, 108 | "call-signature", 109 | "parameter", 110 | "property-declaration" 111 | ], 112 | "typedef-whitespace": [ 113 | true, 114 | { 115 | "call-signature": "nospace", 116 | "index-signature": "nospace", 117 | "parameter": "nospace", 118 | "property-declaration": "nospace", 119 | "variable-declaration": "nospace" 120 | }, 121 | { 122 | "call-signature": "onespace", 123 | "index-signature": "onespace", 124 | "parameter": "onespace", 125 | "property-declaration": "onespace", 126 | "variable-declaration": "onespace" 127 | } 128 | ], 129 | "variable-name": false, 130 | "whitespace": [ 131 | false, 132 | "check-branch", 133 | "check-decl", 134 | "check-operator", 135 | "check-separator", 136 | "check-type" 137 | ], 138 | //https://github.com/jonaskello/tslint-immutable 139 | // :: Immutability rules :: 140 | "readonly-keyword": [ 141 | true, 142 | "ignore-local" 143 | ], 144 | "readonly-array": [ 145 | true, 146 | "ignore-local" 147 | ], 148 | "no-let": true, 149 | // :: Functional style rules :: 150 | //"no-this": true, 151 | //"no-class": true, 152 | //"no-mixed-interface": true, 153 | //"no-expression-statement": true, 154 | //https://github.com/jonaskello/tslint-divid 155 | // :: Other rules :: 156 | "no-arguments": true, 157 | "no-label": true, 158 | //"no-semicolon-interface": true, 159 | "import-containment": [ 160 | true, 161 | { 162 | "containmentPath": "path/to/libs", 163 | "allowedExternalFileNames": [ 164 | "index" 165 | ], 166 | "disallowedInternalFileNames": [ 167 | "index" 168 | ] 169 | } 170 | ] 171 | }, 172 | "defaultSeverity": "warning" 173 | } -------------------------------------------------------------------------------- /src/animatedChartJSNodeCanvas.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error moduleResolution:nodenext issue 54523 2 | import { Chart as ChartJS, ChartConfiguration } from 'chart.js/auto'; 3 | import { ChartJSNodeCanvasBase, MimeType, Canvas } from './chartJSNodeCanvasBase'; 4 | 5 | const animationFrameProvider: AnimationFrameProvider = { 6 | cancelAnimationFrame: (handle) => clearImmediate(handle as any), 7 | requestAnimationFrame: (callback) => setImmediate(() => callback(Date.now())) as any, 8 | }; 9 | 10 | type OnProgress = (chart: ChartJS, progress: number, initial: boolean) => void; 11 | type OnComplete = (chart: ChartJS, initial: boolean) => void; 12 | 13 | export class AnimatedChartJSNodeCanvas extends ChartJSNodeCanvasBase { 14 | 15 | /** 16 | * Render to a data url array. 17 | * @see https://github.com/Automattic/node-canvas#canvastodataurl 18 | * 19 | * @param configuration The Chart JS configuration for the chart to render. 20 | * @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. 21 | */ 22 | public renderToDataURL(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise> { 23 | 24 | const frames: Array = []; 25 | return new Promise((resolve, _reject) => { 26 | this.renderChart(configuration, (chart) => { 27 | 28 | const canvas = chart.canvas as Canvas; 29 | if (!canvas) { 30 | throw new Error('Canvas is null'); 31 | } 32 | const dataUrl = canvas.toDataURL(mimeType); 33 | frames.push(dataUrl); 34 | }, (chart) => { 35 | 36 | resolve(frames); 37 | chart.destroy(); 38 | }); 39 | }); 40 | } 41 | 42 | /** 43 | * Render to a buffer. 44 | * @see https://github.com/Automattic/node-canvas#canvastobuffer 45 | * 46 | * @param configuration The Chart JS configuration for the chart to render. 47 | * @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. 48 | */ 49 | public renderToBuffer(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise> { 50 | 51 | return new Promise((resolve, _reject) => { 52 | 53 | const frames: Array = []; 54 | this.renderChart(configuration, (chart) => { 55 | 56 | const canvas = chart.canvas as Canvas; 57 | if (!canvas) { 58 | throw new Error('Canvas is null'); 59 | } 60 | const buffer = canvas.toBuffer(mimeType); 61 | frames.push(buffer); 62 | }, (chart) => { 63 | 64 | resolve(frames); 65 | chart.destroy(); 66 | }); 67 | }); 68 | } 69 | 70 | private renderChart(configuration: ChartConfiguration, onProgress: OnProgress, onComplete: OnComplete): ChartJS { 71 | 72 | const canvas = this._createCanvas(this._width, this._height, this._type); 73 | (canvas as any).style = (canvas as any).style || {}; 74 | const options = Object.assign({}, configuration.options); 75 | options.responsive = false; 76 | const animation = options.animation || {}; 77 | if (!animation.duration) { 78 | animation.duration = 1000; 79 | } 80 | const baseOnProgress = animation.onProgress; 81 | animation.onProgress = (event) => { 82 | const currentStep: number = (event as any).currentStep; // type docs wrong? 83 | const initial = !!(event as any).initial ? (event as any).initial : false; // added around 3.2.x 84 | const progress = currentStep / event.numSteps; 85 | if (baseOnProgress) { 86 | //baseOnComplete(event.chart); 87 | baseOnProgress.call(animation as any, event); 88 | } 89 | onProgress(event.chart, progress, initial); 90 | }; 91 | const baseOnComplete = animation.onProgress; 92 | animation.onComplete = (event) => { 93 | const initial = !!(event as any).initial ? (event as any).initial : false; // added around 3.2.x 94 | if (baseOnComplete) { 95 | //baseOnComplete(event.chart); 96 | baseOnComplete.call(animation as any, event); 97 | } 98 | onComplete(event.chart, initial); 99 | }; 100 | const plugins = configuration.plugins || []; 101 | const configuredChartConfig = { ...configuration, options, plugins }; 102 | global.window = global.window || {}; 103 | global.window.requestAnimationFrame = animationFrameProvider.requestAnimationFrame; 104 | global.window.cancelAnimationFrame = animationFrameProvider.cancelAnimationFrame; 105 | const context = canvas.getContext('2d'); 106 | (global as any).Image = this._image; // Some plugins use this API 107 | const chart = new this._chartJs((context as any), configuredChartConfig); 108 | delete (global as any).Image; 109 | return chart; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## ChartJSNodeCanvas 4 | **Kind**: global class 5 | 6 | * [ChartJSNodeCanvas](#ChartJSNodeCanvas) 7 | * [new ChartJSNodeCanvas(options)](#new_ChartJSNodeCanvas_new) 8 | * [.renderToDataURL(configuration, mimeType)](#ChartJSNodeCanvas+renderToDataURL) 9 | * [.renderToDataURLSync(configuration, mimeType)](#ChartJSNodeCanvas+renderToDataURLSync) 10 | * [.renderToBuffer(configuration, mimeType)](#ChartJSNodeCanvas+renderToBuffer) 11 | * [.renderToBufferSync(configuration, mimeType)](#ChartJSNodeCanvas+renderToBufferSync) 12 | * [.renderToStream(configuration, mimeType)](#ChartJSNodeCanvas+renderToStream) 13 | * [.registerFont(path, options)](#ChartJSNodeCanvas+registerFont) 14 | 15 | 16 | 17 | ### new ChartJSNodeCanvas(options) 18 | Create a new instance of CanvasRenderService. 19 | 20 | 21 | | Param | Description | 22 | | --- | --- | 23 | | options | Configuration for this instance | 24 | 25 | 26 | 27 | ### chartJSNodeCanvas.renderToDataURL(configuration, mimeType) 28 | Render to a data url. 29 | 30 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 31 | **See**: https://github.com/Automattic/node-canvas#canvastodataurl 32 | 33 | | Param | Default | Description | 34 | | --- | --- | --- | 35 | | configuration | | The Chart JS configuration for the chart to render. | 36 | | mimeType | image/png | The image format, `image/png` or `image/jpeg`. | 37 | 38 | 39 | 40 | ### chartJSNodeCanvas.renderToDataURLSync(configuration, mimeType) 41 | Render to a data url synchronously. 42 | 43 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 44 | **See**: https://github.com/Automattic/node-canvas#canvastodataurl 45 | 46 | | Param | Default | Description | 47 | | --- | --- | --- | 48 | | configuration | | The Chart JS configuration for the chart to render. | 49 | | mimeType | image/png | The image format, `image/png` or `image/jpeg`. | 50 | 51 | 52 | 53 | ### chartJSNodeCanvas.renderToBuffer(configuration, mimeType) 54 | Render to a buffer. 55 | 56 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 57 | **See**: https://github.com/Automattic/node-canvas#canvastobuffer 58 | 59 | | Param | Default | Description | 60 | | --- | --- | --- | 61 | | configuration | | The Chart JS configuration for the chart to render. | 62 | | mimeType | image/png | 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. | 63 | 64 | 65 | 66 | ### chartJSNodeCanvas.renderToBufferSync(configuration, mimeType) 67 | Render to a buffer synchronously. 68 | 69 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 70 | **See**: https://github.com/Automattic/node-canvas#canvastobuffer 71 | 72 | | Param | Default | Description | 73 | | --- | --- | --- | 74 | | configuration | | The Chart JS configuration for the chart to render. | 75 | | mimeType | image/png | 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. | 76 | 77 | 78 | 79 | ### chartJSNodeCanvas.renderToStream(configuration, mimeType) 80 | Render to a stream. 81 | 82 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 83 | **See**: https://github.com/Automattic/node-canvas#canvascreatepngstream 84 | 85 | | Param | Default | Description | 86 | | --- | --- | --- | 87 | | configuration | | The Chart JS configuration for the chart to render. | 88 | | mimeType | image/png | 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. | 89 | 90 | 91 | 92 | ### chartJSNodeCanvas.registerFont(path, options) 93 | 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. 94 | 95 | **Kind**: instance method of [ChartJSNodeCanvas](#ChartJSNodeCanvas) 96 | 97 | | Param | Description | 98 | | --- | --- | 99 | | path | The path to the font file. | 100 | | options | The font options. | 101 | 102 | **Example** 103 | ```js 104 | registerFont('comicsans.ttf', { family: 'Comic Sans' }); 105 | ``` 106 | -------------------------------------------------------------------------------- /resources/nodejs-logo.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/chartJSNodeCanvas.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | // @ts-expect-error moduleResolution:nodenext issue 54523 3 | import { Chart as ChartJS, ChartConfiguration, ChartComponentLike } from 'chart.js/auto'; 4 | import { ChartJSNodeCanvasBase, MimeType, Canvas } from './chartJSNodeCanvasBase'; 5 | 6 | export class ChartJSNodeCanvas extends ChartJSNodeCanvasBase { 7 | 8 | /** 9 | * Render to a data url. 10 | * @see https://github.com/Automattic/node-canvas#canvastodataurl 11 | * 12 | * @param configuration The Chart JS configuration for the chart to render. 13 | * @param mimeType The image format, `image/png` or `image/jpeg`. 14 | */ 15 | public renderToDataURL(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise { 16 | 17 | const chart = this.renderChart(configuration); 18 | return new Promise((resolve, reject) => { 19 | if (!chart.canvas) { 20 | return reject(new Error('Canvas is null')); 21 | } 22 | const canvas = chart.canvas as Canvas; 23 | canvas.toDataURL(mimeType, (error: Error | null, png: string) => { 24 | chart.destroy(); 25 | if (error) { 26 | return reject(error); 27 | } 28 | return resolve(png); 29 | }); 30 | }); 31 | } 32 | 33 | /** 34 | * Render to a data url synchronously. 35 | * @see https://github.com/Automattic/node-canvas#canvastodataurl 36 | * 37 | * @param configuration The Chart JS configuration for the chart to render. 38 | * @param mimeType The image format, `image/png` or `image/jpeg`. 39 | */ 40 | public renderToDataURLSync(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): string { 41 | 42 | const chart = this.renderChart(configuration); 43 | if (!chart.canvas) { 44 | throw new Error('Canvas is null'); 45 | } 46 | const canvas = chart.canvas as Canvas; 47 | const dataUrl = canvas.toDataURL(mimeType); 48 | // Use this to destroy any chart instances that are created. 49 | // This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. 50 | // This must be called before the canvas is reused for a new chart. 51 | chart.destroy(); 52 | return dataUrl; 53 | } 54 | 55 | /** 56 | * Render to a buffer. 57 | * @see https://github.com/Automattic/node-canvas#canvastobuffer 58 | * 59 | * @param configuration The Chart JS configuration for the chart to render. 60 | * @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. 61 | */ 62 | public renderToBuffer(configuration: ChartConfiguration, mimeType: MimeType = 'image/png'): Promise { 63 | 64 | const chart = this.renderChart(configuration); 65 | return new Promise((resolve, reject) => { 66 | if (!chart.canvas) { 67 | throw new Error('Canvas is null'); 68 | } 69 | const canvas = chart.canvas as Canvas; 70 | canvas.toBuffer((error: Error | null, buffer: Buffer) => { 71 | chart.destroy(); 72 | if (error) { 73 | return reject(error); 74 | } 75 | return resolve(buffer); 76 | }, mimeType); 77 | }); 78 | } 79 | 80 | /** 81 | * Render to a buffer synchronously. 82 | * @see https://github.com/Automattic/node-canvas#canvastobuffer 83 | * 84 | * @param configuration The Chart JS configuration for the chart to render. 85 | * @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. 86 | */ 87 | public renderToBufferSync(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' | 'image/svg+xml' = 'image/png'): Buffer { 88 | 89 | const chart = this.renderChart(configuration); 90 | if (!chart.canvas) { 91 | throw new Error('Canvas is null'); 92 | } 93 | const canvas = chart.canvas as Canvas; 94 | const buffer = canvas.toBuffer(mimeType); 95 | chart.destroy(); 96 | return buffer; 97 | } 98 | 99 | /** 100 | * Render to a stream. 101 | * @see https://github.com/Automattic/node-canvas#canvascreatepngstream 102 | * 103 | * @param configuration The Chart JS configuration for the chart to render. 104 | * @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. 105 | */ 106 | public renderToStream(configuration: ChartConfiguration, mimeType: MimeType | 'application/pdf' = 'image/png'): Readable { 107 | 108 | const chart = this.renderChart(configuration); 109 | if (!chart.canvas) { 110 | throw new Error('Canvas is null'); 111 | } 112 | const canvas = chart.canvas as Canvas; 113 | setImmediate(() => chart.destroy()); 114 | switch (mimeType) { 115 | case 'image/png': 116 | return canvas.createPNGStream(); 117 | case 'image/jpeg': 118 | return canvas.createJPEGStream(); 119 | case 'application/pdf': 120 | return canvas.createPDFStream(); 121 | default: 122 | throw new Error(`Un-handled mimeType: ${mimeType}`); 123 | } 124 | } 125 | 126 | private renderChart(configuration: ChartConfiguration): ChartJS { 127 | 128 | const canvas = this._createCanvas(this._width, this._height, this._type); 129 | (canvas as any).style = (canvas as any).style || {}; 130 | configuration.options = configuration.options || {}; 131 | configuration.options.responsive = false; 132 | // Disable animation (otherwise charts will throw exceptions) 133 | configuration.options.animation = false; 134 | const context = canvas.getContext('2d'); 135 | (global as any).Image = this._image; // Some plugins use this API 136 | const chart = new this._chartJs((context as any), configuration); 137 | delete (global as any).Image; 138 | return chart; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/chartJSNodeCanvasBase.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | // @ts-expect-error moduleResolution:nodenext issue 54523 3 | import { Chart as ChartJS, ChartComponentLike } from 'chart.js/auto'; 4 | import { createCanvas, registerFont, Image } from 'canvas'; 5 | import { join as pathJoin } from 'path'; 6 | import { freshRequire } from './freshRequire'; 7 | import { BackgroundColourPlugin } from './backgroundColourPlugin'; 8 | 9 | export type ChartJSNodeCanvasPlugins = { 10 | /** 11 | * Global plugins, see https://www.chartjs.org/docs/latest/developers/plugins.html. 12 | */ 13 | readonly modern?: ReadonlyArray; 14 | /** 15 | * This will work for plugins that `require` ChartJS themselves. 16 | */ 17 | readonly requireChartJSLegacy?: ReadonlyArray; 18 | /** 19 | * This should work for any plugin that expects a global Chart variable. 20 | */ 21 | readonly globalVariableLegacy?: ReadonlyArray; 22 | /** 23 | * This will work with plugins that just return a plugin object and do no specific loading themselves. 24 | */ 25 | readonly requireLegacy?: ReadonlyArray; 26 | }; 27 | export type ChartCallback = (chartJS: typeof ChartJS) => void | Promise; 28 | export type CanvasType = 'pdf' | 'svg'; 29 | export type MimeType = 'image/png' | 'image/jpeg'; 30 | 31 | // https://github.com/Automattic/node-canvas#non-standard-apis 32 | export type Canvas = HTMLCanvasElement & { 33 | toBuffer(callback: (err: Error | null, result: Buffer) => void, mimeType?: string, config?: any): void; 34 | toBuffer(mimeType?: string, config?: any): Buffer; 35 | createPNGStream(config?: any): Readable; 36 | createJPEGStream(config?: any): Readable; 37 | createPDFStream(config?: any): Readable; 38 | }; 39 | 40 | export interface ChartJSNodeCanvasOptions { 41 | /** 42 | * The width of the charts to render, in pixels. 43 | */ 44 | readonly width: number; 45 | /** 46 | * The height of the charts to render, in pixels. 47 | */ 48 | readonly height: number; 49 | /** 50 | * Optional callback which is called once with a new ChartJS global reference as the only parameter. 51 | */ 52 | readonly chartCallback?: ChartCallback; 53 | /** 54 | * Optional canvas type ('PDF' or 'SVG'), see the [canvas pdf doc](https://github.com/Automattic/node-canvas#pdf-output-support). 55 | */ 56 | readonly type?: CanvasType; 57 | /** 58 | * Optional plugins to register. 59 | */ 60 | readonly plugins?: ChartJSNodeCanvasPlugins; 61 | /** 62 | * 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. 63 | */ 64 | readonly backgroundColour?: string; 65 | } 66 | 67 | export abstract class ChartJSNodeCanvasBase { 68 | 69 | protected readonly _width: number; 70 | protected readonly _height: number; 71 | protected readonly _chartJs: typeof ChartJS; 72 | protected readonly _createCanvas: typeof createCanvas; 73 | protected readonly _registerFont: typeof registerFont; 74 | protected readonly _image: typeof Image; 75 | protected readonly _type?: CanvasType; 76 | 77 | /** 78 | * Create a new instance of CanvasRenderService. 79 | * 80 | * @param options Configuration for this instance 81 | */ 82 | constructor(options: ChartJSNodeCanvasOptions) { 83 | 84 | if (options === null || typeof (options) !== 'object') { 85 | throw new Error('An options parameter object is required'); 86 | } 87 | if (!options.width || typeof (options.width) !== 'number') { 88 | throw new Error('A width option is required'); 89 | } 90 | if (!options.height || typeof (options.height) !== 'number') { 91 | throw new Error('A height option is required'); 92 | } 93 | 94 | this._width = options.width; 95 | this._height = options.height; 96 | const canvas = freshRequire('canvas'); 97 | this._createCanvas = canvas.createCanvas; 98 | this._registerFont = canvas.registerFont; 99 | this._image = canvas.Image; 100 | this._type = options.type && options.type.toLowerCase() as CanvasType; 101 | this._chartJs = this.initialize(options); 102 | } 103 | 104 | /** 105 | * 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. 106 | * 107 | * @param path The path to the font file. 108 | * @param options The font options. 109 | * @example 110 | * registerFont('comicsans.ttf', { family: 'Comic Sans' }); 111 | */ 112 | public registerFont(path: string, options: { readonly family: string, readonly weight?: string, readonly style?: string }): void { 113 | 114 | this._registerFont(path, options); 115 | } 116 | 117 | protected initialize(options: ChartJSNodeCanvasOptions): typeof ChartJS { 118 | 119 | const chartJs: typeof ChartJS = require('chart.js/auto'); 120 | 121 | if (options.plugins?.requireChartJSLegacy) { 122 | for (const plugin of options.plugins.requireChartJSLegacy) { 123 | require(plugin); 124 | delete require.cache[require.resolve(plugin)]; 125 | } 126 | } 127 | 128 | if (options.plugins?.globalVariableLegacy) { 129 | (global as any).Chart = chartJs; 130 | for (const plugin of options.plugins.globalVariableLegacy) { 131 | freshRequire(plugin); 132 | } 133 | delete (global as any).Chart; 134 | } 135 | 136 | if (options.plugins?.modern) { 137 | for (const plugin of options.plugins.modern) { 138 | if (typeof plugin === 'string') { 139 | chartJs.register(freshRequire(plugin)); 140 | } else { 141 | chartJs.register(plugin); 142 | } 143 | } 144 | } 145 | 146 | if (options.plugins?.requireLegacy) { 147 | for (const plugin of options.plugins.requireLegacy) { 148 | chartJs.register(freshRequire(plugin)); 149 | } 150 | } 151 | 152 | if (options.chartCallback) { 153 | options.chartCallback(chartJs); 154 | } 155 | 156 | if (options.backgroundColour) { 157 | chartJs.register(new BackgroundColourPlugin(options.width, options.height, options.backgroundColour)); 158 | } 159 | const chartJsPath = pathJoin('node_modules','chart.js'); 160 | 161 | for (const key of Object.keys(require.cache)) { 162 | if (key.includes(chartJsPath)) { 163 | delete require.cache[key]; 164 | } 165 | } 166 | 167 | return chartJs; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Assert } from 'ts-std-lib'; 2 | import { describe, it } from 'mocha'; 3 | // @ts-expect-error moduleResolution:nodenext issue 54523 4 | import { ChartConfiguration } from 'chart.js/auto'; 5 | 6 | import { ChartJSNodeCanvas, ChartCallback, CanvasType, MimeType, ChartJSNodeCanvasPlugins } from './'; 7 | 8 | const assert = new Assert(); 9 | 10 | describe(ChartJSNodeCanvas.name, () => { 11 | 12 | // const chartColors = { 13 | // red: 'rgb(255, 99, 132)', 14 | // orange: 'rgb(255, 159, 64)', 15 | // yellow: 'rgb(255, 205, 86)', 16 | // green: 'rgb(75, 192, 192)', 17 | // blue: 'rgb(54, 162, 235)', 18 | // purple: 'rgb(153, 102, 255)', 19 | // grey: 'rgb(201, 203, 207)' 20 | // }; 21 | const width = 400; 22 | const height = 400; 23 | const configuration: ChartConfiguration = { 24 | type: 'bar', 25 | data: { 26 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 27 | datasets: [{ 28 | label: '# of Votes', 29 | data: [12, 19, 3, 5, 2, 3], 30 | backgroundColor: [ 31 | 'rgba(255, 99, 132, 0.2)', 32 | 'rgba(54, 162, 235, 0.2)', 33 | 'rgba(255, 206, 86, 0.2)', 34 | 'rgba(75, 192, 192, 0.2)', 35 | 'rgba(153, 102, 255, 0.2)', 36 | 'rgba(255, 159, 64, 0.2)' 37 | ], 38 | borderColor: [ 39 | 'rgba(255,99,132,1)', 40 | 'rgba(54, 162, 235, 1)', 41 | 'rgba(255, 206, 86, 1)', 42 | 'rgba(75, 192, 192, 1)', 43 | 'rgba(153, 102, 255, 1)', 44 | 'rgba(255, 159, 64, 1)' 45 | ], 46 | borderWidth: 1 47 | }] 48 | }, 49 | options: { 50 | scales: { 51 | y: { 52 | ticks: { 53 | beginAtZero: true, 54 | callback: (value: number) => '$' + value 55 | } as any 56 | } 57 | } 58 | }, 59 | plugins: { 60 | annotation: { 61 | } 62 | } as any 63 | }; 64 | 65 | function createSUT(type?: CanvasType, plugins?: ChartJSNodeCanvasPlugins): ChartJSNodeCanvas { 66 | 67 | const chartCallback: ChartCallback = (ChartJS) => { 68 | 69 | ChartJS.defaults.responsive = true; 70 | ChartJS.defaults.maintainAspectRatio = false; 71 | }; 72 | return new ChartJSNodeCanvas({ width, height, chartCallback, type, plugins }); 73 | } 74 | 75 | const mimeTypes: ReadonlyArray = ['image/png', 'image/jpeg']; 76 | 77 | describe(ChartJSNodeCanvas.prototype.renderToDataURL.name, () => { 78 | 79 | describe(`given canvasType 'undefined'`, () => { 80 | 81 | const canvasType = undefined; 82 | 83 | mimeTypes.forEach((mimeType) => { 84 | 85 | describe(`given mimeType '${mimeType}'`, () => { 86 | 87 | it('renders data url', async () => { 88 | const chartJSNodeCanvas = createSUT(canvasType); 89 | const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration, mimeType); 90 | assert.equal(dataUrl.startsWith(`data:${mimeType};base64,`), true); 91 | }); 92 | 93 | it('renders data url in parallel', async () => { 94 | const chartJSNodeCanvas = createSUT(canvasType); 95 | const promises = Array(3).fill(undefined).map(() => chartJSNodeCanvas.renderToDataURL(configuration, mimeType)); 96 | const dataUrls = await Promise.all(promises); 97 | dataUrls.forEach((dataUrl) => assert.equal(dataUrl.startsWith(`data:${mimeType};base64,`), true)); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | 104 | describe(ChartJSNodeCanvas.prototype.renderToDataURLSync.name, () => { 105 | 106 | describe(`given canvasType 'undefined'`, () => { 107 | 108 | const canvasType = undefined; 109 | 110 | mimeTypes.forEach((mimeType) => { 111 | 112 | describe(`given mimeType '${mimeType}'`, () => { 113 | 114 | it('renders data url', () => { 115 | const chartJSNodeCanvas = createSUT(canvasType); 116 | const dataUrl = chartJSNodeCanvas.renderToDataURLSync(configuration, mimeType); 117 | assert.equal(dataUrl.startsWith(`data:${mimeType};base64,`), true); 118 | }); 119 | 120 | it('renders data url in parallel', () => { 121 | const chartJSNodeCanvas = createSUT(canvasType); 122 | const dataUrls = Array(3).fill(undefined).map(() => chartJSNodeCanvas.renderToDataURLSync(configuration, mimeType)); 123 | dataUrls.forEach((dataUrl) => assert.equal(dataUrl.startsWith(`data:${mimeType};base64,`), true)); 124 | }); 125 | }); 126 | }); 127 | }); 128 | }); 129 | 130 | describe(ChartJSNodeCanvas.prototype.renderToBuffer.name, () => { 131 | 132 | describe(`given canvasType 'undefined'`, () => { 133 | 134 | const canvasType = undefined; 135 | 136 | mimeTypes.forEach((mimeType) => { 137 | 138 | describe(`given extended mimeType '${mimeType}'`, () => { 139 | 140 | it('renders chart', async () => { 141 | const chartJSNodeCanvas = createSUT(canvasType); 142 | const image = await chartJSNodeCanvas.renderToBuffer(configuration, mimeType); 143 | assert.equal(image instanceof Buffer, true); 144 | }); 145 | }); 146 | }); 147 | }); 148 | }); 149 | 150 | describe(ChartJSNodeCanvas.prototype.renderToBufferSync.name, () => { 151 | 152 | ([ 153 | [undefined, mimeTypes], 154 | ['svg', ['image/svg+xml']], 155 | ['pdf', ['application/pdf']] 156 | ] as ReadonlyArray<[CanvasType, ReadonlyArray]>).forEach(([canvasType, extendedMimeTypes]) => { 157 | 158 | describe(`given canvasType '${canvasType}'`, () => { 159 | 160 | extendedMimeTypes.forEach((mimeType) => { 161 | 162 | describe(`given mimeType '${mimeType}'`, () => { 163 | 164 | it('renders chart', async () => { 165 | const chartJSNodeCanvas = createSUT(canvasType); 166 | const image = chartJSNodeCanvas.renderToBufferSync(configuration, mimeType); 167 | assert.equal(image instanceof Buffer, true); 168 | }); 169 | }); 170 | }); 171 | }); 172 | }); 173 | }); 174 | 175 | describe(ChartJSNodeCanvas.prototype.renderToStream.name, () => { 176 | 177 | ([ 178 | [undefined, mimeTypes], 179 | ['pdf', ['application/pdf']] 180 | ] as ReadonlyArray<[CanvasType | undefined, ReadonlyArray]>).forEach(([canvasType, extendedMimeTypes]) => { 181 | 182 | describe(`given canvasType '${canvasType}'`, () => { 183 | 184 | extendedMimeTypes.forEach((mimeType) => { 185 | 186 | describe(`given extended mimeType '${mimeType}'`, () => { 187 | 188 | it('renders stream', (done) => { 189 | const chartJSNodeCanvas = createSUT(canvasType); 190 | const stream = chartJSNodeCanvas.renderToStream(configuration, mimeType); 191 | const data: Array = []; 192 | stream.on('data', (chunk: Buffer) => { 193 | data.push(chunk); 194 | }); 195 | stream.on('end', () => { 196 | assert.equal(Buffer.concat(data).length > 0, true); 197 | done(); 198 | }); 199 | stream.on('finish', () => { 200 | assert.equal(Buffer.concat(data).length > 0, true); 201 | done(); 202 | }); 203 | stream.on('error', (error) => { 204 | done(error); 205 | }); 206 | }); 207 | }); 208 | }); 209 | }); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | # chartjs-node-canvas 7 | 8 | 9 | [![GitHub](https://github.com/SeanSobey/ChartjsNodeCanvas/workflows/Node%20CI/badge.svg)](https://github.com/SeanSobey/ChartjsNodeCanvas/actions) 10 | [![codecov](https://codecov.io/gh/SeanSobey/ChartjsNodeCanvas/branch/master/graph/badge.svg)](https://codecov.io/gh/SeanSobey/ChartjsNodeCanvas) 11 | [![NPM](https://img.shields.io/npm/v/chartjs-node-canvas.svg)](https://www.npmjs.com/package/chartjs-node-canvas) 12 | [![packagephobia publish](https://badgen.net/packagephobia/publish/chartjs-node-canvas@latest)](https://bundlephobia.com/result?p=chartjs-node-canvas) 13 | 14 | 15 | 16 | A Node JS renderer for [Chart.js](http://www.chartjs.org) using [canvas](https://github.com/Automattic/node-canvas). 17 | 18 | Provides and alternative to [chartjs-node](https://www.npmjs.com/package/chartjs-node) that does not require jsdom (or the global variables that this requires) and allows chartJS as a peer dependency, so you can manage its version yourself. 19 | 20 | ## Contents 21 | 22 | 1. [Installation](#installation) 23 | 2. [Node JS version](#node-js-version) 24 | 3. [Features](#features) 25 | 4. [Limitations](#limitations) 26 | 5. [API](#api) 27 | 6. [Usage](#usage) 28 | 7. [Known Issues](#known-issues) 29 | 8. [Sponsors](#sponsors) 30 | 31 | ## Installation 32 | 33 | ``` 34 | npm i chartjs-node-canvas chart.js 35 | ``` 36 | 37 | ### Node JS version 38 | 39 | This is limited by the upstream dependency [canvas](https://github.com/Automattic/node-canvas). 40 | 41 | See the GitHub Actions [yml](.github/workflows/nodejs.yml) section for the current supported Node version(s). You will need to do a `npm rebuild` to rebuild the canvas binaries. 42 | 43 | ### Charts.JS version 44 | 45 | Currently supports 4.x.x. You are given the ability to maintain the version yourself via peer dependency, but be aware that going above the specified [version](./package.json) might result in errors. 46 | 47 | ## Features 48 | 49 | * Supports all Chart JS features and charts. 50 | * No heavy DOM virtualization libraries, thanks to a [pull request](https://github.com/chartjs/Chart.js/pull/5324) to chart.js allowing it to run natively on node, requiring only a Canvas API. 51 | * Chart JS is a peer dependency, so you can bump and manage it yourself. 52 | * Provides a callback with the global ChartJS variable, so you can use the [Global Configuration](https://www.chartjs.org/docs/latest/configuration/#global-configuration). 53 | * Uses (similar to) [fresh-require](https://www.npmjs.com/package/fresh-require) for each instance of `ChartJSNodeCanvas`, so you can mutate the ChartJS global variable separately within each instance. 54 | * Support for custom fonts. 55 | 56 | ## Limitations 57 | 58 | ### Node Modules 59 | 60 | I hope to convert this package to a node module in the future, but since it uses the CommonJS API to manage memory for ChartJS this is not a simple task. It it a top priority for the next major release. 61 | 62 | ### Animations 63 | 64 | Chart animation (and responsive resize) is disabled by this library. This is necessary since the animation API's required are not available in Node JS/canvas-node (this is not a browser environment after all). 65 | 66 | This is the same as: 67 | 68 | ```js 69 | Chart.defaults.animation = false; 70 | Chart.defaults.responsive = false; 71 | ``` 72 | 73 | *Note this is WIP, see the [change log](CHANGELOG.md) for most recent development. 74 | 75 | ### SVG and PDF 76 | 77 | For some unknown reason canvas requires use of the [sync](https://github.com/Automattic/node-canvas#canvastobuffer) API's to use SVG's or PDF's. This libraries which support these are: 78 | 79 | * [renderToBufferSync](./API.md#ChartJSNodeCanvas+renderToBufferSync) ('application/pdf' | 'image/svg+xml') 80 | * [renderToStream](./API.md#ChartJSNodeCanvas+renderToStream) ('application/pdf') 81 | 82 | You also need to set the canvas type when you initialize the `ChartJSNodeCanvas` instance like the following: 83 | 84 | ```js 85 | const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); 86 | 87 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ type: 'svg', width: 800, height: 600 }); 88 | ``` 89 | 90 | ## API 91 | 92 | See the [API docs](https://github.com/SeanSobey/ChartjsNodeCanvas/blob/master/API.md). 93 | 94 | ## Usage 95 | 96 | ```js 97 | const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); 98 | 99 | const width = 400; //px 100 | const height = 400; //px 101 | const backgroundColour = 'white'; // Uses https://www.w3schools.com/tags/canvas_fillstyle.asp 102 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour}); 103 | 104 | (async () => { 105 | const configuration = { 106 | ... // See https://www.chartjs.org/docs/latest/configuration 107 | }; 108 | const image = await chartJSNodeCanvas.renderToBuffer(configuration); 109 | const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration); 110 | const stream = chartJSNodeCanvas.renderToStream(configuration); 111 | })(); 112 | ``` 113 | 114 | Also see the [example](./src/example.ts) and the generated [image](./example.png). 115 | 116 | ### Memory Management 117 | 118 | Every instance of `ChartJSNodeCanvas` creates its own [canvas](https://github.com/Automattic/node-canvas). To ensure efficient memory and GC use make sure your implementation creates as few instances as possible and reuses them: 119 | 120 | ```js 121 | // Re-use one service, or as many as you need for different canvas size requirements 122 | const smallChartJSNodeCanvas = new ChartJSNodeCanvas({ width: 400, height: 400 }); 123 | const bigCChartJSNodeCanvas = new ChartJSNodeCanvas({ width: 2000, height: 2000 }); 124 | 125 | // Expose just the 'render' methods to downstream code so they don't have to worry about life-cycle management. 126 | exports = { 127 | renderSmallChart: (configuration) => smallChartJSNodeCanvas.renderToBuffer(configuration), 128 | renderBigChart: (configuration) => bigCChartJSNodeCanvas.renderToBuffer(configuration) 129 | }; 130 | ``` 131 | 132 | ### Custom Charts 133 | 134 | Just use the ChartJS reference in the callback: 135 | 136 | ```js 137 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback: (ChartJS) => { 138 | // New chart type example: https://www.chartjs.org/docs/latest/developers/charts.html 139 | class MyType extends Chart.DatasetController { 140 | 141 | } 142 | 143 | Chart.register(MyType); 144 | } 145 | }); 146 | ``` 147 | 148 | ### Global Config 149 | 150 | Just use the ChartJS reference in the callback: 151 | 152 | ```js 153 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback: (ChartJS) => { 154 | // Global config example: https://www.chartjs.org/docs/latest/configuration/ 155 | ChartJS.defaults.elements.line.borderWidth = 2; 156 | } }); 157 | ``` 158 | 159 | ### Custom Fonts 160 | 161 | Just use the `registerFont` method: 162 | 163 | ```js 164 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback: (ChartJS) => { 165 | // Just example usage 166 | ChartJS.global.defaultFontFamily = 'VTKS UNAMOUR'; 167 | } }); 168 | // Register before rendering any charts 169 | chartJSNodeCanvas.registerFont('./testData/VTKS UNAMOUR.ttf', { family: 'VTKS UNAMOUR' }); 170 | ``` 171 | 172 | See the node-canvas [docs](https://github.com/Automattic/node-canvas#registerfont) and the chart js [docs](https://www.chartjs.org/docs/latest/general/fonts.html). 173 | 174 | #### Windows 175 | 176 | On windows you need to install the font first, before running your app. Otherwise you will get an error something like: 177 | `Pango-WARNING **: 11:13:09.211: couldn't load font "vtks unamour Not-Rotated 12px", falling back to "Sans Not-Rotated 12px", expect ugly output.` 178 | 179 | See [here](https://github.com/Automattic/node-canvas/issues/1643). 180 | 181 | ### Background color 182 | 183 | Due to the many issues and question this includes a [convenience plugin](./src/backgroundColourPlugin.ts) to fill the otherwise transparent background. It uses the [fillStyle](https://www.w3schools.com/tags/canvas_fillstyle.asp) canvas API; 184 | 185 | ```js 186 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour: 'purple' }); 187 | ``` 188 | 189 | ### Loading plugins 190 | 191 | This library is designed to make loading plugins as simple as possible. For legacy plugins, you should just be able to add the module name to the appropriate array option and the library handles the rest. 192 | 193 | The Chart.JS [plugin API](https://www.chartjs.org/docs/latest/developers/plugins.html) has changed over time and this requires compatibility options for the different ways plugins have been historically loaded. ChartJS Node Canvas has a `plugin` option with specifiers for the different ways supported plugin loading methods are handled. If you are not sure about your plugin, just try the different ones until your plugin loads: 194 | 195 | #### Newer plugins 196 | 197 | Let `ChartJSNodeCanvas` manage the lifecycle of the plugin itself, each instance will have a separate instance of the plugin: 198 | 199 | ```js 200 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 201 | modern: ['chartjs-plugin-annotation'] 202 | } }); 203 | ``` 204 | 205 | You want to share the plugin instance, this may cause unwanted issues, use at own risk: 206 | 207 | ```js 208 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 209 | modern: [require('chartjs-plugin-annotation')] 210 | } }); 211 | ``` 212 | 213 | #### Older plugins 214 | 215 | --- 216 | 217 | 1. Plugin that expects a global Chart variable. 218 | 219 | ```js 220 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 221 | requireChartJSLegacy: [''] 222 | }}); 223 | ``` 224 | 225 | 2. Plugins that `require` ChartJS themselves. 226 | 227 | ```js 228 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 229 | globalVariableLegacy: ['chartjs-plugin-crosshair'] 230 | } }); 231 | ``` 232 | 233 | 3. Register plugin directly with ChartJS: 234 | 235 | ```js 236 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 237 | requireLegacy: ['chartjs-plugin-datalabels'] 238 | } }); 239 | ``` 240 | 241 | --- 242 | 243 | These approaches can be combined also: 244 | 245 | ```js 246 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, plugins: { 247 | modern: ['chartjs-plugin-annotation'], 248 | requireLegacy: ['chartjs-plugin-datalabels'] 249 | } }); 250 | ``` 251 | 252 | See the [tests](src/index.e2e.spec.ts#106) for some examples. 253 | 254 | ## Known Issues 255 | 256 | There is a problem with persisting config objects between render calls, see this [issue](https://github.com/SeanSobey/ChartjsNodeCanvas/issues/9) for details and workarounds. 257 | 258 | ## Sponsors 259 | 260 | @athombv at https://homey.app 261 | -------------------------------------------------------------------------------- /testData/win32/render-to-data-URL.txt: -------------------------------------------------------------------------------- 1 | data:image/png;base64,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 -------------------------------------------------------------------------------- /src/index.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { AssertionError } from 'assert'; 2 | import { promises as fs } from 'fs'; 3 | import { platform, EOL } from 'os'; 4 | import { join } from 'path'; 5 | import { Readable } from 'stream'; 6 | import { describe, it } from 'mocha'; 7 | import { Stream } from 'stream'; 8 | // @ts-expect-error moduleResolution:nodenext issue 54523 9 | import { ChartConfiguration } from 'chart.js/auto'; 10 | import resemble /*, { ResembleSingleCallbackComparisonOptions, ResembleSingleCallbackComparisonResult }*/ from 'resemblejs'; 11 | 12 | import { ChartJSNodeCanvas, ChartCallback } from './'; 13 | 14 | describe(ChartJSNodeCanvas.name, () => { 15 | 16 | const chartColors = { 17 | red: 'rgb(255, 99, 132)', 18 | orange: 'rgb(255, 159, 64)', 19 | yellow: 'rgb(255, 205, 86)', 20 | green: 'rgb(75, 192, 192)', 21 | blue: 'rgb(54, 162, 235)', 22 | purple: 'rgb(153, 102, 255)', 23 | grey: 'rgb(201, 203, 207)' 24 | }; 25 | const width = 400; 26 | const height = 400; 27 | const configuration: ChartConfiguration = { 28 | type: 'bar', 29 | data: { 30 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 31 | datasets: [{ 32 | label: '# of Votes', 33 | data: [12, 19, 3, 5, 2, 3], 34 | backgroundColor: [ 35 | 'rgba(255, 99, 132, 0.2)', 36 | 'rgba(54, 162, 235, 0.2)', 37 | 'rgba(255, 206, 86, 0.2)', 38 | 'rgba(75, 192, 192, 0.2)', 39 | 'rgba(153, 102, 255, 0.2)', 40 | 'rgba(255, 159, 64, 0.2)' 41 | ], 42 | borderColor: [ 43 | 'rgba(255,99,132,1)', 44 | 'rgba(54, 162, 235, 1)', 45 | 'rgba(255, 206, 86, 1)', 46 | 'rgba(75, 192, 192, 1)', 47 | 'rgba(153, 102, 255, 1)', 48 | 'rgba(255, 159, 64, 1)' 49 | ], 50 | borderWidth: 1 51 | }] 52 | }, 53 | options: { 54 | scales: { 55 | y: { 56 | beginAtZero: true, 57 | ticks: { 58 | callback: (tickValue, _index, _ticks) => '$' + tickValue 59 | } 60 | } 61 | }, 62 | plugins: { 63 | annotation: { 64 | } 65 | } as any 66 | } 67 | }; 68 | const chartCallback: ChartCallback = (ChartJS) => { 69 | ChartJS.defaults.responsive = true; 70 | ChartJS.defaults.maintainAspectRatio = false; 71 | }; 72 | 73 | it('works with render to buffer', async () => { 74 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback, backgroundColour: 'white' }); 75 | const actual = await chartJSNodeCanvas.renderToBuffer(configuration); 76 | await assertImage(actual, 'render-to-buffer'); 77 | }); 78 | 79 | it('works with render to data url', async () => { 80 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback, backgroundColour: 'white' }); 81 | const actual = await chartJSNodeCanvas.renderToDataURL(configuration); 82 | const extension = '.txt'; 83 | const fileName = 'render-to-data-URL'; 84 | const fileNameWithExtension = fileName + extension; 85 | const expectedDataPath = join(process.cwd(), 'testData', platform(), fileName + extension); 86 | const expected = await fs.readFile(expectedDataPath, 'utf8'); 87 | // const result = actual === expected; 88 | const compareData = await compareImages(actual, expected, { output: { useCrossOrigin: false } }); 89 | const misMatchPercentage = Number(compareData.misMatchPercentage); 90 | const result = misMatchPercentage > 0; 91 | if (result) { 92 | const actualDataPath = expectedDataPath.replace(fileNameWithExtension, fileName + '-actual' + extension); 93 | await fs.writeFile(actualDataPath, actual); 94 | const compare = `
Actual:
${EOL}${EOL}
Expected:
`; 95 | const compareDataPath = expectedDataPath.replace(fileNameWithExtension, fileName + '-compare.html'); 96 | await fs.writeFile(compareDataPath, compare); 97 | const diffPng = compareData.getBuffer(); 98 | await writeDiff(expectedDataPath.replace(fileNameWithExtension, fileName + '-diff' + extension), diffPng); 99 | throw new AssertionError({ 100 | message: `Expected data urls to match, mismatch was ${misMatchPercentage}%${EOL}See '${compareDataPath}'`, 101 | actual: actualDataPath, 102 | expected: expectedDataPath, 103 | }); 104 | } 105 | }); 106 | 107 | it('works with render to stream', async () => { 108 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback, backgroundColour: 'white' }); 109 | const stream = chartJSNodeCanvas.renderToStream(configuration); 110 | const actual = await streamToBuffer(stream); 111 | await assertImage(actual, 'render-to-stream'); 112 | }); 113 | 114 | it('works with registering plugin', async () => { 115 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ 116 | width, height, backgroundColour: 'white', plugins: { 117 | modern: ['chartjs-plugin-annotation'] 118 | } 119 | }); 120 | const actual = await chartJSNodeCanvas.renderToBuffer({ 121 | type: 'bar', 122 | data: { 123 | labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], 124 | datasets: [ 125 | { 126 | type: 'line', 127 | label: 'Dataset 1', 128 | borderColor: chartColors.blue, 129 | borderWidth: 2, 130 | fill: false, 131 | data: [-39, 44, -22, -45, -27, 12, 18] 132 | }, 133 | { 134 | type: 'bar', 135 | label: 'Dataset 2', 136 | backgroundColor: chartColors.red, 137 | data: [-18, -43, 36, -37, 1, -1, 26], 138 | borderColor: 'white', 139 | borderWidth: 2 140 | }, 141 | { 142 | type: 'bar', 143 | label: 'Dataset 3', 144 | backgroundColor: chartColors.green, 145 | data: [-7, 21, 1, 7, 34, -29, -36] 146 | } 147 | ] 148 | }, 149 | options: { 150 | responsive: true, 151 | plugins: { 152 | annotation: { 153 | annotations: { 154 | label1: { 155 | type: 'label', 156 | xValue: 3, 157 | yValue: 20, 158 | backgroundColor: 'rgba(245,245,245)', 159 | content: ['This is my text', 'This is my text, second line'], 160 | font: { 161 | size: 18 162 | } 163 | } 164 | } 165 | } 166 | } as any 167 | } 168 | }); 169 | await assertImage(actual, 'chartjs-plugin-annotation'); 170 | }); 171 | 172 | it('works with self registering plugin', async () => { 173 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ 174 | width, height, backgroundColour: 'white', plugins: { 175 | requireLegacy: [ 176 | 'chartjs-plugin-datalabels' 177 | ] 178 | } 179 | }); 180 | const actual = await chartJSNodeCanvas.renderToBuffer({ 181 | type: 'bar', 182 | data: { 183 | labels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as any, 184 | datasets: [{ 185 | label: 'Red', 186 | backgroundColor: chartColors.red, 187 | data: [12, 19, 3, 5, 2, 3], 188 | datalabels: { 189 | align: 'end', 190 | anchor: 'start' 191 | } 192 | }, { 193 | label: 'Blue', 194 | backgroundColor: chartColors.blue, 195 | data: [3, 5, 2, 3, 30, 15, 19, 2], 196 | datalabels: { 197 | align: 'center', 198 | anchor: 'center' 199 | } 200 | }, { 201 | label: 'Green', 202 | backgroundColor: chartColors.green, 203 | data: [12, 19, 3, 5, 2, 3], 204 | datalabels: { 205 | anchor: 'end', 206 | align: 'start', 207 | } 208 | }] as any 209 | }, 210 | options: { 211 | plugins: { 212 | datalabels: { 213 | color: 'white', 214 | display(context: any): boolean { 215 | return context.dataset.data[context.dataIndex] > 15; 216 | }, 217 | font: { 218 | weight: 'bold' 219 | }, 220 | formatter: Math.round 221 | } 222 | } as any, // TODO: resolve type 223 | scales: { 224 | x: { 225 | stacked: true 226 | }, 227 | y: { 228 | stacked: true 229 | } 230 | } 231 | } 232 | }); 233 | await assertImage(actual, 'chartjs-plugin-datalabels'); 234 | }); 235 | 236 | // it.skip('works with global variable plugin', async () => { 237 | // const chartJSNodeCanvas = new ChartJSNodeCanvas({ 238 | // width, height, backgroundColour: 'white', plugins: { 239 | // globalVariableLegacy: [ 240 | // 'chartjs-plugin-crosshair' 241 | // ] 242 | // } 243 | // }); 244 | // const actual = await chartJSNodeCanvas.renderToBuffer({ 245 | // }); 246 | // await assertImage(actual, 'chartjs-plugin-funnel'); 247 | // }); 248 | 249 | it('works with custom font', async () => { 250 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ 251 | width, height, backgroundColour: 'white', chartCallback: (ChartJS) => { 252 | ChartJS.defaults.font.family = 'Anthrope'; 253 | } 254 | }); 255 | chartJSNodeCanvas.registerFont('./testData/Anthrope.ttf', { family: 'Anthrope' }); 256 | const actual = await chartJSNodeCanvas.renderToBuffer({ 257 | type: 'bar', 258 | data: { 259 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 260 | datasets: [{ 261 | label: '# of Votes', 262 | data: [12, 19, 3, 5, 2, 3], 263 | backgroundColor: [ 264 | 'rgba(255, 99, 132, 0.2)', 265 | 'rgba(54, 162, 235, 0.2)', 266 | 'rgba(255, 206, 86, 0.2)', 267 | 'rgba(75, 192, 192, 0.2)', 268 | 'rgba(153, 102, 255, 0.2)', 269 | 'rgba(255, 159, 64, 0.2)' 270 | ], 271 | borderColor: [ 272 | 'rgba(255,99,132,1)', 273 | 'rgba(54, 162, 235, 1)', 274 | 'rgba(255, 206, 86, 1)', 275 | 'rgba(75, 192, 192, 1)', 276 | 'rgba(153, 102, 255, 1)', 277 | 'rgba(255, 159, 64, 1)' 278 | ], 279 | borderWidth: 1, 280 | }] 281 | }, 282 | options: { 283 | scales: { 284 | y: { 285 | ticks: { 286 | beginAtZero: true, 287 | callback: (value: number) => '$' + value 288 | } as any 289 | } 290 | } 291 | }, 292 | plugins: { 293 | annotation: { 294 | } 295 | } as any 296 | }); 297 | await assertImage(actual, 'font'); 298 | }); 299 | 300 | it('works without background color', async () => { 301 | 302 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback }); 303 | const actual = await chartJSNodeCanvas.renderToBuffer(configuration); 304 | await assertImage(actual, 'no-background-color'); 305 | }); 306 | 307 | /* 308 | function hashCode(string: string): number { 309 | 310 | let hash = 0; 311 | if (string.length === 0) { 312 | return hash; 313 | } 314 | for (let i = 0; i < string.length; i++) { 315 | const chr = string.charCodeAt(i); 316 | hash = ((hash << 5) - hash) + chr; 317 | hash |= 0; // Convert to 32bit integer 318 | } 319 | return hash; 320 | } 321 | */ 322 | 323 | async function assertImage(actual: Buffer, fileName: string): Promise { 324 | const extension = '.png'; 325 | const fileNameWithExtension = fileName + extension; 326 | const testDataPath = join(process.cwd(), 'testData', platform(), fileNameWithExtension); 327 | const exists = await pathExists(testDataPath); 328 | if (!exists) { 329 | console.error(`Warning: expected image path does not exist!, creating '${testDataPath}'`); 330 | await fs.writeFile(testDataPath, actual, 'base64'); 331 | return; 332 | } 333 | const expected = await fs.readFile(testDataPath); 334 | const compareData = await compareImages(actual, expected); 335 | // const compareData = await new Promise((resolve) => { 336 | // const diff = resemble(actual) 337 | // .compareTo(expected) 338 | // .onComplete((data) => { 339 | // resolve(data); 340 | // }); 341 | // }); 342 | const misMatchPercentage = Number(compareData.misMatchPercentage); 343 | // const result = actual.equals(expected); 344 | const result = misMatchPercentage > 0; 345 | // const actual = hashCode(image.toString('base64')); 346 | // const expected = -1377895140; 347 | // assert.equal(actual, expected); 348 | if (result) { 349 | await fs.writeFile(testDataPath.replace(fileNameWithExtension, fileName + '-actual' + extension), actual); 350 | const diffPng = compareData.getBuffer(); 351 | await writeDiff(testDataPath.replace(fileNameWithExtension, fileName + '-diff' + extension), diffPng); 352 | throw new AssertionError({ 353 | message: `Expected image to match '${testDataPath}', mismatch was ${misMatchPercentage}%'`, 354 | // actual: JSON.stringify(actual), 355 | // expected: JSON.stringify(expected), 356 | operator: 'to equal', 357 | stackStartFn: assertImage, 358 | }); 359 | } 360 | } 361 | 362 | // resemblejs/compareImages 363 | //function compareImages(image1: string | Buffer, image2: string | Buffer, options?: ResembleSingleCallbackComparisonOptions): Promise { 364 | function compareImages(image1: string | Buffer, image2: string | Buffer, options?: any): Promise { 365 | return new Promise((resolve, reject) => { 366 | //resemble.compare(image1, image2, options || {}, (err, data) => { 367 | resemble.compare(image1, image2, options || {}, (err: any, data: any) => { 368 | if (err) { 369 | reject(err); 370 | } else { 371 | resolve(data); 372 | } 373 | }); 374 | }); 375 | } 376 | 377 | function streamToBuffer(stream: Readable): Promise { 378 | const data: Array = []; 379 | return new Promise((resolve, reject) => { 380 | stream.on('data', (chunk: Buffer) => { 381 | data.push(chunk); 382 | }); 383 | stream.on('end', () => { 384 | const buffer = Buffer.concat(data); 385 | resolve(buffer); 386 | }); 387 | stream.on('error', (error) => { 388 | reject(error); 389 | }); 390 | }); 391 | } 392 | 393 | function writeDiff(filepath: string, png: Stream | Buffer): Promise { 394 | return new Promise((resolve, reject) => { 395 | if (Buffer.isBuffer(png)) { 396 | fs.writeFile(filepath, png.toString('base64'), 'base64') 397 | .then(() => resolve()) 398 | .catch(reject); 399 | return; 400 | } 401 | const chunks: Array = []; 402 | png.on('data', (chunk: Uint8Array) => { 403 | chunks.push(chunk); 404 | }); 405 | png.on('end', () => { 406 | const buffer = Buffer.concat(chunks); 407 | fs.writeFile(filepath, buffer.toString('base64'), 'base64') 408 | .then(() => resolve()) 409 | .catch(reject); 410 | }); 411 | png.on('error', (err) => reject(err)); 412 | }); 413 | } 414 | 415 | function pathExists(path: string): Promise { 416 | return fs.access(path).then(() => true).catch(() => false); 417 | } 418 | 419 | describe('Memory tests', () => { 420 | 421 | const count = 20; 422 | // TODO: Replace node-memwatch with a new lib! 423 | 424 | it('does not leak with new instance parallel', async () => { 425 | 426 | const memoryUsages = new Array(count + 2); 427 | memoryUsages[0] = process.memoryUsage(); 428 | const diffs = await Promise.all([...Array(count).keys()].map((iteration) => { 429 | console.log('generated heap for iteration ' + (iteration + 1)); 430 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback }); 431 | return chartJSNodeCanvas.renderToBuffer(configuration, 'image/png') 432 | .then(() => { 433 | memoryUsages[iteration + 1] = process.memoryUsage(); 434 | return 1; 435 | }); 436 | })); 437 | memoryUsages[count + 1] = process.memoryUsage(); 438 | 439 | const config = generateMemoryUsageChartConfig(memoryUsages, 'New Instance Test'); 440 | const buffer = await new ChartJSNodeCanvas({ width: 800, height: 600 }).renderToBuffer(config); 441 | await fs.writeFile('./resources/memory-usage-new-instance-parallel.png', buffer); 442 | }); 443 | 444 | it('does not leak with new instance sequential', async () => { 445 | 446 | const memoryUsages = new Array(count + 2); 447 | memoryUsages[0] = process.memoryUsage(); 448 | for (let index = 0; index < count; index++) { 449 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback }); 450 | await chartJSNodeCanvas.renderToBuffer(configuration, 'image/png'); 451 | memoryUsages[index + 1] = process.memoryUsage(); 452 | 453 | } 454 | memoryUsages[count + 1] = process.memoryUsage(); 455 | 456 | const config = generateMemoryUsageChartConfig(memoryUsages, 'New Instance Test'); 457 | const buffer = await new ChartJSNodeCanvas({ width: 800, height: 600 }).renderToBuffer(config); 458 | await fs.writeFile('./resources/memory-usage-new-instance-sequential.png', buffer); 459 | }); 460 | 461 | it('does not leak with same instance', async () => { 462 | 463 | const memoryUsages = new Array(count + 2); 464 | memoryUsages[0] = process.memoryUsage(); 465 | const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback }); 466 | const diffs = await Promise.all([...Array(count).keys()].map((iteration) => { 467 | return chartJSNodeCanvas.renderToBuffer(configuration, 'image/png') 468 | .then(() => { 469 | memoryUsages[iteration + 1] = process.memoryUsage(); 470 | return 1; 471 | }); 472 | })); 473 | memoryUsages[count + 1] = process.memoryUsage(); 474 | const config = generateMemoryUsageChartConfig(memoryUsages, 'Same Instance Test'); 475 | const buffer = await new ChartJSNodeCanvas({ width: 800, height: 600 }).renderToBuffer(config); 476 | await fs.writeFile('./resources/memory-usage-same-instance.png', buffer); 477 | }); 478 | 479 | function generateMemoryUsageChartConfig(memoryUsages: NodeJS.MemoryUsage[], _testName: string): ChartConfiguration { 480 | const labels = memoryUsages.map((_, index) => `Iteration ${index}`); 481 | const data = { 482 | labels, 483 | datasets: [ 484 | { 485 | label: 'RSS', 486 | data: memoryUsages.map(usage => usage.rss / 1000000), // Convert to MB 487 | backgroundColor: 'rgba(255, 99, 132, 0.2)', 488 | borderColor: 'rgba(255, 99, 132, 1)', 489 | borderWidth: 1 490 | }, 491 | { 492 | label: 'Heap Total', 493 | data: memoryUsages.map(usage => usage.heapTotal / 1000000), // Convert to MB 494 | backgroundColor: 'rgba(54, 162, 235, 0.2)', 495 | borderColor: 'rgba(54, 162, 235, 1)', 496 | borderWidth: 1 497 | }, 498 | { 499 | label: 'Heap Used', 500 | data: memoryUsages.map(usage => usage.heapUsed / 1000000), // Convert to MB 501 | backgroundColor: 'rgba(75, 192, 192, 0.2)', 502 | borderColor: 'rgba(75, 192, 192, 1)', 503 | borderWidth: 1 504 | }, 505 | { 506 | label: 'External', 507 | data: memoryUsages.map(usage => usage.external / 1000000), // Convert to MB 508 | backgroundColor: 'rgba(153, 102, 255, 0.2)', 509 | borderColor: 'rgba(153, 102, 255, 1)', 510 | borderWidth: 1 511 | } 512 | ] 513 | }; 514 | 515 | return { 516 | type: 'line', 517 | data, 518 | options: { 519 | responsive: true, 520 | maintainAspectRatio: false, 521 | scales: { 522 | y: { 523 | beginAtZero: true, 524 | ticks: { 525 | callback: (value: number) => `${value} MB` 526 | } as any 527 | } 528 | } 529 | } 530 | }; 531 | } 532 | }); 533 | }); 534 | -------------------------------------------------------------------------------- /testData/linuxw/render-to-data-URL.txt: -------------------------------------------------------------------------------- 1 | data:image/png;base64,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RURkIPnMQHbs2MHChQsZPnw4s2fP5gc/+IE3PLrj8XjYtWsXOTk5rFmzhhUrVnDkyJE+L1pERAaezwwkJSWFpUuX4na7TznD8KehoYHW1lZef/11srOzaW9v5/e//z0//vGPGTduXKe2FRUVXfbjcDhITEz022awcTgcftc7nRbATO2xWtwhnv4pyoD6VhNgwel0UlHR0GW77sY7GA21MWu8g59PgPz4xz9m1apVbN26lfvuu4+cnBy+853vYLVae9zpOeecwwUXXACAxWLh/fff9wmQ5OTkbvvpSZvBxN947UecQBPRMdHEhAbu2dfmZjfQgN1uJznZ7rftUHt+YeiNWeMd3HzeiSIiIrj66quZOXMmv/zlL6mpqWHNmjU96iwiIgKLxcLw4cO9y4YPH86xY8f6rmIREQkIPgHS0tLi/Tk2NpaxY8fS0ND1rohOnZnNTJ06lQMHDniXHThwYMilsojIUOCzC+v+++8nKyuLiooK3nrrLbZv386dd97pXb9z50527drFgQMH8Hg8vPLKK0yaNIns7GwALr/8cp544gkqKytpb2/nwIED3HXXXf03IhER6Rc+AbJo0SL27NlDTU0No0aN4vLLLyc6Otq7PioqirS0NNLS0rzLTl4/YsQIFi1aRFFREcHBwVx//fXY7f73hYuIyJnHJ0BsNhvZ2dkcPHiQnJwcbDZbp/WjRo1i1KhRfjuNjIw85RXqIiIyeHR5Ok92drbPbUxERERO6PJeWN3NMkREZGgL3AsKREQkoClARETEEAWIiIgYogARERFDFCAiImKIAkRERAxRgIiIiCEKEBERMUQBIiIihihARETEEAWIiIgYogARERFDFCAiImKIAkRERAxRgIiIiCEKEBERMUQBIiIihihARETEEAWIiIgYogARERFDugyQHTt20Nra2p+1iIjIGaTLADl48CDt7e0+y8vLy1m7di3/+Mc/2Lx5c5cdu1wuXn/9ddauXds3lYqISEAJ+uoCl8vFtm3b2L9/PyaTieTkZHJycggK6mi6bt06GhoaOHr0KACzZs06ZcerV69m8+bNpKWlcf7555/GIYiIyEDwCZAXXngBl8tFREQEsbGx7N+/n4kTJxIREQHAwoULAViyZAkul+uUnR49epRPP/2UefPmcfDgwdNYvoiIDBSfXVi7du3immuuITExkZycHL773e96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P2Gz2UhPT2fSpEkEBwf7tPN4PLz00ktMmzbtlMdAKioquvwdDoeDxMREv20SXC7cbW3U19T0dgj9KszpxA7U1NTQFtZ1XjscDr/9OJ0WwEztsVrcIYH7fQL1rSbAgtPppKKioct23Y03uLWGODqOtTW5A/s5jmprw+xxUeVne4Xux+xyuWijY1sJZM7GjlvX19TUUOF0dtmuu/GW7Qyjam9on9Z2OiVkNjNyWlOX67vdpuv+d5tudNIU4M9xVFsbZlf323SvA+R73/seu3fvZvny5bz11lu8/vrr/OxnPyMhIcHbxuPxsGTJEurr67nzzjtP2U9ycnK3v8tvG4sFS3AwcXFxvR1C/7JXAnTU2c2Y/Y3XfsQJNBEdE01MaOCefW1udgMN2O12kpPtftv6fX6dTjgGdrsde0yAP8fHg8Fl+drbtOXQIYLPgG3a7nZDXR1xcXEkR0b6betvvIc2wJH8vq7u9IkbHkpysv9juX63gaA6AOzhduwB/hwTHAx0v00bOgtr/PjxFBUVMX/+fN566y0++ugjrr32Wu/6N998k4MHD/Kzn/0Mq9Vq5FeIyBDgMcElt0OY/xwaUE0NsOIvA11FYOr1R9m9e/d2+n97ezshISHe/7/99tvs3r2bn/70p4SFhX39CkVEJCD1egbyzjvvUF9fT3t7O9u2bcNut/Ptb38b6DjFd+XKlURGRvLwww97H3PZZZdx9tln913VIiIy4HodIL/4xS9obW3lzTff5KKLLup0htWwYcP43e9+5/MYu93/vnARETnzGDoGEhISQlhYmE8wmM3mgD8AKCIifcPw6TwXX3yxjnGIiAxhhu+Fpd1SIiJDW+BeUCAiIgFNASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiiAJEREQMUYCIiIghChARETFEASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiiAJEREQMUYCIiIghChARETFEASIiIoYoQERExBAFiIiIGGI4QD744AOam5v7shYRETmDBBl9oNPpxO12+yxva2vj7bffZteuXYSFhXHJJZeQnZ39tYoUEZHA0+sAqa6uZtmyZezevZs9e/aQmJjId77zHUJDQwFYvnw5+/bt49Zbb6WyspIXXniB++67j+Tk5D4vXkREBk6vd2E999xzpKamMmPGDG6++WaysrLweDwAuN1uPvnk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 -------------------------------------------------------------------------------- /testData/linux/render-to-data-URL-actual.txt: -------------------------------------------------------------------------------- 1 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAACAvzbMAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nO3deWBU1d3/8fdMMtknG5MNSEASCDsKCIJSRS3qg+LeqqjUR30o+qhYH7UiimKhtspSrUWtVUSBIlYtFcQWEBSwgAFkV8ISCAkhk3WSSSaZ5fcHP6cMkIVrNsjn9Rc399yT75kZ5jPn3Js7Jp/P50NEROQMmVu7ABEROTspQERExBAFiIiIGKIAERERQxQgIiJiiAJEREQMUYCIiIghChARETEkuLULkHPUki9bu4KGjflJa1cgclZTgEjzWPIV/OSC1q6ibl9uUYCI/EgKEGk+bT1A6lFWVsaKFSu4+eabycrKIjg4mAEDBjS6+9zcXP74xz8SFxfHk08+CcDu3bt55513ePHFFzGb/7N67PP5eOqppxg3bhy9evU6bX+FhYU4HA66devW6BpEmpvOgYicRl5eHikpKQDk5+f7/91Ye/fuJSMjwx8eAD179iQoKIg9e/YEtP3uu+8wmUz07Nmzzv6+/fZbvv/++zOqQaS5aQYichp5eXn+T/t2ux2bzXZKm8rKSubPn8/hw4cJCgriwgsv5L/+67/47rvv+Pzzz6mtrWXOnDlMmDABAJPJxEUXXcSGDRvo3bu3v58NGzZw0UUX4XQ6T9vf3r17Wb58OUFBQbhcLq666ioWLlzI/v378Xq9XH755VxxxRUA7Ny5kw8//BCPx4PZbOaWW26hb9++LfCISXukABE5QWlpKXPnzuXo0aMkJCQQFBTEkSNHeOWVV7jvvvuIioryt/34448JCwtj6tSpVFdXM336dLp06ULfvn356U9/Sl5eHnfddVdA/8OGDWPatGm4XC5CQ0Opra1ly5YtTJo0qd7+Bg8eTGxsLFdffTUffPABPp+P559/nqqqKqZNm0aPHj1ITU3lww8/ZOzYsWRkZJCbm8vq1asVINJstIQlcoLY2FgmTpxIRkYGjz32GBMmTKBfv35MnDgxIDwAduzYwYgRIzCZTISHh3P++eeza9euevtPTEykc+fObN26FTi+NNWpUycSExMb3d/27du59NJLMZvNREZGMnDgQH9/0dHRbNy4EbvdTufOnbnzzjub6JEROZVmICIncbvdWCwW4PjJ8NTU1NO2czgcWK1W/3ZUVBRHjhxpsP9hw4axYcMGhg4dyoYNGxg2bNgZ9VdWVsZbb73lPxHvdrsZOHAgAPfeey/Lli3jpZdeIjw8XEtY0qwUICInyMrK4l//+hdOp5PZs2dTXFxMeHg4eXl5jB07NqBtTEwMFRUV/vMjFRUVREdHN/g7Bg8ezOLFiykoKGDv3r3ce++9Z9RfdHQ048ePP22wRUdHc9ttt/Hzn/+cbdu28ec//5lZs2YFXPUl0lT0qhI5waBBg7jooosYN24cEydOJD09nccff/yU8ADo168fa9euxefzUVlZyebNm+nXr1+DvyMsLIwBAwawYMEC+vfvT1hYWIP9mc1mKioqABgwYABffvklPp8Pj8fD4sWLOXjwILW1tfz2t7+lpKQEk8lEp06dMJlMmEymJnyERP5DMxCRk+Tk5HDxxRcD4PF4CA4+/X+T66+/ngULFjBlyhSCgoIYOXIkmZmZjfodw4YNY/bs2UycOLFR/fXr1485c+ZQWlrKnXfeycKFC5k8eTImk4k+ffqQmppKUFAQl156KTNnzsTr9WKxWBg3bpwCRJqNSd+JLs3ivmkw+b9bu4q6/eZteOvp1q5C5KymGYg0nwb+2ltEzm4KEGkeY0a0dgX1a+v1iZwFtIQlIiKG6CosERExRAEiIiKGKEBERMSQgJPoPp+PDz/8kG+++QaHw0FcXBxjxoxh6NChwPH79nz00UeUlpaSlpbGnXfeSVJSEnD8vkB/+9vfKCsro2vXrowbN46YmJiWH5GIiLSIgBlIVlYWhw4d4je/+Q09evRgwoQJLFy4kNLSUkpKSnjnnXe4++67mT17NhkZGcyfPx8Ap9PJ22+/ze23387LL79Mx44d+eSTT1plQCIi0jICAqSsrIyUlBT/jeQ6d+7M448/TmRkJCaTibvuuov09HRMJhMDBw6koKAAOP6FOKmpqfTo0QOz2cxVV13Fpk2b8Hq9LT8iERFpEQFLWH379mXp0qXExMTgdrvx+Xx06tQJOH6b60GDBvnb7tq1i+7duwNQUlJCYmKif5/VasVisVBeXk5sbGxLjENERFpYwAwkKSmJxx9/nJKSErKzs3nqqaf46quvTjlo586dfPnll9x8883A8W9mCw0NDWgTHh5OZWVlM5YuIiKt6ZS/RE9JSeHOO++ksLCQ6667jjfffJPExET/Td02btzIp59+yiOPPEJcXBxw/HsLysrKAvpxOp0B320Ax79bui4FBQX+E/LthcbcPmjM7UN7HHNAgGzatImMjAzi4uIwmUxkZGRwwQUXcPjwYTIzM/n2229Zvnw5jz32WMAVVh06dCArK8u/XVJSgtfrPSVAUlJSGiyoMW3OJe1tvKAxtxca87kvYAlr3759/O1vf6OmpgaA8vJy/wlyp9PJwoULefDBB0+5PLd3794UFhayZ88evF4vn332GcOGDdNtpEVEzmEB98JyuVwsWLCAXbt2UVlZSVxcHCNHjuTKK69k/fr1vPvuu6d0MGPGDKKioti9ezeLFi2ivLyc9PR0fvGLXxAZGdnoQn5Y3mpPCZ6fn9+uxgsac3uhMbcPdd5M8eQvu2luCpD2QWNuHzTm9qHOW5m0ZHiIiMjZR/fCEhERQxQgIiJiiAJEREQMUYCIiIghChARETFEASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiyCnfByJtyxeHXHhPe7eytik9NpiuMUGtXYaItAAFSBs3+SsHtR4fnAW3xjdh4pfnh3NPv4jWLkVEWoAC5CyQHhvM1d1CG27YioqqfCzaXdXaZYhIC1KAnAXiw8wMSQlp7TLqdbjcw6LWLkJEWpROoouIiCEKEBERMUQBIiIihihARETEEAWIiIgYogARERFDFCAiImKIAkRERAypM0Cqq6vrPKiqqgqf7/Q3aKrr5yIicm4J+Et0r9fLggUL2LZtG06nk4iICMaMGcMll1wCgNvtZvHixaxevZoZM2YQFRXlP/brr79m2bJlAFitVu6++26Sk5NbcCgiItKSAmYg33zzDXa7nenTp5ORkcFjjz3Ghx9+SElJCQCvv/46HTp0wGwOnLiUlJTwwQcf8Ktf/YoXXniBIUOGMH/+/JYbhYiItLiAJHA4HCQkJBAcfHxikpSUxKRJk7BarQDcfPPNjBo16pRO7HY7sbGxxMXFAdCjRw+OHj3a3LWLiEgrCljC6tevH0uXLiUiIoKamhq8Xi+JiYn+/SkpKaftJDU1FafTSXZ2Nueddx6bNm2ib9++zVu5iIi0qoAASUxMZNKkSaxatYoDBw7wxBNPcM0113DFFVfU20lYWBg33XQTs2fPJjQ0FIvFwq9+9atT2uXn59fZR0FBAUlJSfW2OdcUFBQ0opUFV40Lu71t3yq91GkCgnA4HOTnl9XZrnFjPrdozO1DexzzKbdzt9ls/OxnPyMvL4+bbrqJP/3pT3Ts2JFevXrV2cnhw4dZsmQJ06dPJzo6mh07dvCHP/yBF154IeB8SV0zmBM1ps25pOHx2gkNCcVmi2yReoyqKvcADqxWKykp9X+hVHt7jkFjbi/a25gDzoGsX7+eoqIi/3ZaWhoDBgzgyJEj9Xby/fff061bN6KjowHo27cvpaWllJXV/UlURETObgEBcuTIERYvXozT6QSgqKiI3bt306VLl3o76dy5M3v37qW8vByAbdu2ERERQUxMTDOVLSIirS1gCeuGG27ggw8+YOrUqTgcDl599VWuvPJKunfvTmVlJZMnTwaOn/N45plnAJg6dSqZmZn89Kc/ZebMmXg8HqKjo5kwYcIpl/uKiMi5IyBALBYLY8eOBWDOnDlMmDDBvy8yMpJZs2bV2dEVV1zR4Ml2ERE5d9Q5RTgxPERERE6mNSYRETFEASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiiAJEREQMUYCIiIghChARETFEASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiiAJEREQMUYCIiIghChARETFEASIiIoYoQERExBAFiIiIGKIAERERQxQgIiJiyGkDxOv14nQ68fl8pz2ovn3V1dVNV52IiLRZwSdueDwe3n33Xfbs2UNVVRWhoaGMHj2akSNHAlBTU8PChQtZv349M2bMICoqyn/s0aNHeeONNygrK8NisXD//feTkZHRsqMREZEWEzAD2bRpE5WVlbz44oukp6fz1FNPsWTJEoqKigB4/fXX6dKlC2bzqROX999/nyuvvJKZM2cyduxYNm3a1DIjEBGRVhGQBE6nk9jYWH9AdOjQgWeffZa4uDgAbrvtNi677LJTOrHb7ZSUlDB8+HAA+vfvz+23397ctYuISCsKWMIaMGAAy5YtIyQkhOrqatxutz88ABITE0/bSUFBAXFxcbzxxhvk5OQQERHB3XffTZcuXZq3ehERaTUBAdKhQweefvpp1qxZw+HDh3n88ccZNWoU11xzTb2dOBwODh48yJNPPklqairr16/nrbfe4oUXXghol5+fX2cfBQUFJCUl1dvmXFNQUNCIVhZcNS7s9qpmr+fHKHWagCAcDgf5+WV1tmvcmM8tGnP70B7HHHzyD+Li4rjhhhs4ePAgP//5z3n11VdJTU2lb9++dXYSHh5OcnIyqampAAwbNowFCxbgcDiwWq3+dikpKQ0W1Jg255KGx2snNCQUmy2yReoxqqrcAxx/vlNSIupt296eY9CY24v2NuaAcyBffvkldrvdv52SkkK/fv04evRovZ0kJCTgcDj8l/Z6PB68Xi8Wi6UZShYRkbYgIEDsdjsLFy6krOz4EkR+fj47d+6kW7du+Hw+nE4nTqcTwP9vn89Hx44diYuL41//+hfV1dV89tlndOvWjbCwsJYfkYiItIiAJawxY8bw8ccfM2PGDIqKinjnnXcYPXo03bp1o7KyksmTJwMQFhbGb3/7WwCmTp2K1Wrl3nvvZf78+axatYq0tDTuvffelh+NiIi0mIAACQ4O5tZbb+XWW29lzpw5TJgwwb8vMjKSWbNm1dlRQkICEydObL5KRUSkTanzXlgnhoeIiMjJdDNFERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYshpA8TlclFUVITb7T7tQXa7Ha/XW2endrsdj8fTNBWKiEibFHzihtvt5q233uLQoUNUVlYSFBTEVVddxVVXXQVAdXU17733Ht988w0zZswgKirqlA43b97MG2+8wfPPP09ycnLLjEJERFpcwAxk48aNeL1epk2bxnnnncezzz7L8uXLsdvtALz++uv06dMHs/n0K1/V1dUsWbKEuLi45q9cRERaVUASuFwuIiMjMZlMAMTGxjJ16lRsNhsA48aNY/jw4XV29sknnzBixAjCw8ObsWQREWkLAgLk/PPPZ8eOHcybN4+qqipcLhdWq9W/v76ZxcGDBzlw4AAjR45svmpFRKTNCDgHEhcXx7PPPsvatWvZuHEjjz/+OJdffjnXX3+9f1ZyOl6vlwULFjB27Ng6l7cA8vPz69xXUFBAUlJSvW3ONQUFBY1oZcFV48Jur2r2en6MUqcJCMLhcJCfX1Znu8aN+dyiMbcP7XHMwSf/wGq1cs011/Ddd99x1113MXv2bLp160b//v3r7GTVqlV07tyZhIQEnE4nPp+P6upq3G43wcH/+RUpKSkNFtSYNueShsdrJzQkFJstskXqMaqq3AM4sFqtpKRE1Nu2vT3HoDG3F+1tzAHThZUrVwakaIcOHejduzfHjh2rtxO73U5ubi6zZ89m9uzZ2O123n33XbKzs5unahERaXUBM5DKykr/UpTH4yEnJ4ft27fzP//zP/h8Pqqq/rOM4nQ6MZvNhIeHc9tttwV0+vzzzzN+/Hhdxisicg4LCJDRo0ezdOlS3nzzTY4ePcqHH37IzTffTNeuXXE6nUybNg2A+Ph4/vCHPwDw61//OuBEOxw/lxIUFNRCQxARkdYQECBBQUGMGTOGMWPG8M4773DPPff490VERPgDpCEPP/xw01YpIiJtTp2XTJ0YHiIiIifTzRRFRMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIacNkIqKCux2Oy6X65R9Pp8Pu92O1+s9ZZ/b7aa4uPi0+0RE5NwSfOJGbW0tc+bMwW6343A4CAoK4tJLL+W6664DoLKykrlz57Jt2zZmzJhBVFSU/9gVK1awdOlSYmNjcTqd3HfffXTv3r1lRyMiIi0mYAayceNGQkJCmDp1Kl26dGHKlCl88cUX2O12AP785z8zdOhQzObAicvRo0dZunQpzz77LFOmTOHaa69l8eLFLTcKERFpcQFJUFtbS2hoqH/barUyffp0bDYbAP/93//N4MGDT+kkOjqaRx99lLi4OADS0tJwOBzNWbeIiLSygAC54IIL2LNnD3/5y1+orKyksrKSsLAw//7o6OjTdhIREUFaWpp/e+PGjfTv37+ZShYRkbYg4BxITEwMU6ZM4euvv+bbb7/lqaee4pJLLuHWW2/FZDI1qsPVq1fz3Xff8X//93+n7MvPz6/zuIKCApKSkuptE7FlL+aK6kbV0Ra4E2Opzkytc39BQUEjerHgqnFht1c1XWHNoNRpAoJwOBzk55fV2a5xYz63aMztQ3scc/DJP4iIiOCKK65g+/bt3HPPPcycOZMePXpw/vnnN9jZP/7xD3bv3s2jjz4aMHP5QUpKSoN91NvmzaVw+Cx6kob2gcuG1Nuk4cfETmhIKDZbZNPV1Qyqyj2AA6vVSkpKRL1tG/M6ONdozO1DextzQIB8/vnn9O/f3/8gxMTE0LNnT4qKihrsaNWqVezbt4+JEycSEhLSPNUCxETBLZc3X/9NweeDv/yjtasQEWlWAQHi8Xh4//33uf3223G73ezdu5dvv/2WBx98EK/XS3Fxsb9tUVER1dXVxMfHU1payrJly3j44YcpLy/3t4mPjz/liq0fX3EwpNiats+m5vW1dgUiIs0uIECuvvpqzGYzCxYsIDc3l88++4w77riD1NRUnE4ns2bNAo4Hw5tvvgnAr3/9a/bv309oaChvvPFGQOdPP/00ERH1L2eIiMjZKSBAzGYzV199NVdffTXvvPMO99xzj39fREQE06ZNO20ngwcPPu3lvSIicu6qc33pxPAQERE5mW6mKCIihihARETEEAWIiIgYogARERFDFCAiImKIAkRERAxRgIiIiCEKEBERMUQBIiIihihARETEEAWIiIgYogARERFDFCAiImKIAkRERAxRgIiIiCEKEBERMUQBIiIihihARETEEAWIiIgYogARERFDFCAiImKIAkRERAw5bYAUFxeTk5NDRUXFKft8Ph+HDh3C6/Wess/lcnHs2LHT7hMRkXNL8IkbLpeLV199lerqaoqLiwkODmbo0KHcfPPNADgcDt5++2127drFjBkziIqK8h+7YsUKli9fTnx8PGVlZTz00EN07ty5ZUcjIiItJmAGsnHjRmJjY5k8eTJpaWk899xzfP311xQWFgLwl7/8hZEjR2I2B05cSkpK+PTTT5k0aRKTJk1i9OjRfPzxxy03ChERaXEBSXDy0lNERAQvvvgiCQkJANx3333079//lE6ys7Pp0aMH8fHxAAwdOpTdu3fjdrubq24REWllAQEycOBA9u/fz5/+9CccDgdlZWUEB/9nlevEJasTlZaWEhcX598ODQ0lIiKCsrKyZipbRERaW8A5EKvVypQpU9i4cSN/+9vfeOaZZxgyZAh33HHHKctWJ6qurg4IGgCLxYLL5Qr4WX5+fp19FBQUkJSUVG8bW20tZo+bEru93kG1Oq8PG1BVVUVpA2NumAVXjQu7varJymsOpU4TEITD4SA/v+4PDo0b87lFY24f2uOYg0/+QWhoKCNGjCArK4v777+fl19+ma1btzJw4MA6O4mJiaG4uDjgZxUVFcTExAT8LCUlpcGC6m1jsUCtF5vN1mA/rcrrAyA8PJzwBsbc8GNi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VhbV1dXs3LmTkpKSZilaRERaX6NnIKmpqbz77rtcf/31VFZW8uWXX7J161auuuqqU2YE+/bt47rrrtNlvCIi57BGB8jFF19MVFQU27Zto7S0lIKCAn71q1+ddjmpd+/ehIWFNWmhIiLStpzROZABAwYwYMAAli5dyujRo+tsd+utt/7owkREpG0zdC+s+sJDRETaB91MUUREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBiiABEREUMUICIiYogCREREDFGAiIiIIfruWWlbaksgd25rV3FmksZARHprVyFt3e6PoGB7a1fReFHJMHh8vU0UINK2eKuh9N+tXcWZib8EUIBIAwp3wYEvwNTahTRSfHdAASJnow6Xg+2nrV1F/cq/hfy/tnYVclYxweg/gamNnz1Y/VyjmrXxUYiISFulABEREUMUICIiYogCREREDFGAiIiIIQoQERExRAEiIiKGKEBERMQQBYiIiBhyRn+JXltby969e6mqqiImJob09HRMpv/8XX5OTg4lJSV07twZm83W5MWKiEjb0egAKS8v5/e//z1du3YlOzubmJgYYmJimDBhAiaTibfffpvDhw+TlpbG+++/z0033cTw4cObs3YREWlFjV7CysrKomfPntx3330kJyfz+OOP43A4KCoq4sCBA+zZs4ennnqKe+65h3HjxvH55583Z90iItLKGj0DCQ0Npbi4GJ/Pd/zA4GCefPJJAGw2G7/5zW8ICQkBIDw8HK/X2wzliohIW9HoGcjgwYOpqanh+eefp6ioiO+//94fJoA/PHw+HytWrODiiy9u+mpFRKTNaPQMJCQkhMcee4zs7Gzeeust5s2bR2xsLA8//LA/PLxeL++//z4mk4lRo0ad0kd+fn6d/RcUFJCUlFRvG1ttLWaPmxK7vbFltw6vDxtQVVVFaQNjbpgFV40Lu72qycprDqVOExCEw+EgP7+sznYNjTnYU0gC4HRW4Wzjz3NojQMrUFJSQrXT+PNcUlEBgKOiAnsbn7lXVR1/HRYWFmK2WOps19CY83eHkbcjvElra1YmH4NuKa23SUNjjq2qIhyw2+1t/nbucR4P3tpa7PW8f8EZXoVlMpno3r07KSkpPPzww8ycOZOsrCyGDRuGx+PhzTffxGq1cv/992M2n/oApaSkNPg76m1jsUCtt+1f4eU9PjMLDw8n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 -------------------------------------------------------------------------------- /testData/linux/render-to-data-URL-compare.html: -------------------------------------------------------------------------------- 1 |
Actual:
2 | 3 |
Expected:
--------------------------------------------------------------------------------