Save HTMLCanvasElements as images
Tags: typescript, chartjs
Published: 2020-10-30 10:19:48

## Intro
This time, I want to save charts what are drawn by Chart.js as images.
* [Chart.js](https://www.chartjs.org)

To do this, I will try saving HTMLCanvasElement as an image.

In this time, I just use samples to use Chart.js.
I will try Chart.js next time.

### Environments
* Node.js ver.15.0.0

#### package.json
```json
{
    "browserslist": [
        "last 2 version"
    ],
    "scripts": {
        "css": "npx postcss postcss -c postcss.config.js -d wwwroot/css -w"
    },
    "dependencies": {
        "autoprefixer": "^10.0.1",
        "chart.js": "^2.9.4",
        "postcss": "^8.1.2",
        "postcss-cli": "^8.1.0",
        "postcss-import": "^13.0.0",
        "precss": "^4.0.0",
        "ts-loader": "^8.0.6",
        "tsc": "^1.20150623.0",
        "typescript": "^4.0.3",
        "webpack": "^5.2.0",
        "webpack-cli": "^4.1.0"
    },
    "devDependencies": {
        "@types/chart.js": "^2.9.27",
        "whatwg-fetch": "^3.4.1"
    }
}
```
## Base projects
#### ChartPage.html
```html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" type="text/css" href="../css/chart_page.css" >
    </head>
    <body>
        <div id="outside">
            <h1>Chart</h1>
            <div class="chart_area">
                <canvas id="chart_1" class="chart_canvas"></canvas>
            </div>
        </div>
        <button onclick="Page.save()">Save</button>
        <script src="../js/chartPage.bundle.js"></script>
        <script>
            (function() {
                Page.init();
            })();
        </script>
    </body>
</html>
```
#### chart_page.css
```css
.chart_area{
    background-color: white;
    border: solid black 1px;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 300px;
    width: 300px;
}
.chart_canvas {
}
```
## Draw chart samples
This time, I use the chart sample in Chart.js document.

* [Introduction · Chart.js documentation](https://www.chartjs.org/docs/latest/)

#### chart.page.ts
```typescript
import { SampleDrawer } from "./charts/sample-drawer";

export function init() {
    const target = document.getElementById('chart_1');
    if (target == null)
    {
        return;
    }
    const drawer = new SampleDrawer();
    drawer.draw(target as HTMLCanvasElement);
}
```

#### sample-drawer.ts
```typescript
import Chart from "chart.js";

export class SampleDrawer {
    public draw(canvas: HTMLCanvasElement) {
        const context = canvas.getContext('2d');
        if (context == null) {
            console.error('failed getting context');
            return;
        }
        this.drawSample(canvas);
    }
    private drawSample(canvas: HTMLCanvasElement) {
        const chart = new Chart(canvas, {
            // as same as the sample script at "Creating a Chart"
        });
    }
}
```

#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/31pc65zhp7860uvgcgyo.jpg)

The chart size comes from the parent element's(&lt;div class="chart_area">) width or height.
Though I change the canvas size, background-color, and etc. before instantiating the chart instance, they will be ignored.

## Save as a image
The HTMLCanvasElement can generate Blob or Base64 string and I can use them to create images.
If I save image by a server application, I prefer using "toDataURL" and send Base64 string to the server.

* [HTMLCanvasElement.toDataURL() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)

But this time, I use "toBlob()" to download by TypeScript.

* [HTMLCanvasElement.toBlob() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)

#### chart.page.ts
```typescript
...
export function save() {
    const target = document.getElementById('chart_1');
    if (target == null)
    {
        console.error('chart_1 was not found');
        return;
    }
    const targetCanvas = target as HTMLCanvasElement;
    targetCanvas.toBlob((blob) => downloadImageBlob(blob), 'image/jpeg', 1);
}
function downloadImageBlob(blob: Blob|null) {
    if(blob == null){
        console.error('Failed getting image blob');
        return;
    }
    if (navigator.msSaveBlob == null) {
        if (navigator.msSaveBlob == null) {
        const linkElement = document.getElementById('download_link') as HTMLAnchorElement;
        linkElement.href = window.URL.createObjectURL(blob);
        linkElement.download = 'sample_file.jpg';
        linkElement.click();
        return;
    }
    // for IE
    window.navigator.msSaveBlob(blob, 'sample_file.jpg');
}
```
#### ChartPage.html
```html
...
        <button onclick="Page.save()">Save</button>
        <a id="download_link"></a>
        <script src="../js/polyfill/toBlob.js"></script>
        <script src="../js/chartPage.bundle.js"></script>
...
```
For downloading created image, I add an AnchorElement.

* [javascript - Fetch API Download File - Stack Overflow](https://stackoverflow.com/questions/64294314/fetch-api-download-file)

And For IE, I add a polyfill file.

* [HTMLCanvasElement.toBlob() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)

#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/awuj29i10x1vw1ltd3zv.jpg)

Where did the background color come from?
It's because the chart didn't have background colors, and I saved as Jpeg image(no transparency).

So I have to set background color.
But it ignores canvas background color.

### Set background color
To set background color, I use plugins of Chart.js.

* [Plugins · Chart.js documentation](https://www.chartjs.org/docs/latest/developers/plugins.html)

#### sample-drawer.ts
```typescript
    public draw(canvas: HTMLCanvasElement) {
        const context = canvas.getContext('2d');
        if (context == null) {
            return;
        }
        this.drawBackground();
        this.drawSample(canvas);
    }
    private drawBackground() {
        Chart.pluginService.register({
            beforeDraw: c => {
                const ctx = c.ctx;
                if (ctx == null){
                    return;
                }
                const canvas = c.canvas;
                if (canvas == null) {
                    return;
                }
                ctx.fillStyle = 'rgba(255, 255, 255, 1)';                
                ctx.fillRect(0, 0, canvas.width, canvas.height);
            }
        });
    }
...
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qjlkdss6fcwq4hmzsne4.jpg)

## Hide chart and save image
Can I only want to show an image what is created from a chart dynamically and hide the chart?

### display:none; (Failed)
#### chart_page.css
```css
#outside{
    display: none;
}
...
```

I couldn't get the blob by "toBlob()".

### visibility: hidden; (Success)
#### chart_page.css
```css
#outside{
    visibility: hidden;
}
...
```

### Draw outside (Success)
```css
#outside{
}
.chart_area{
...
    height: 1024px;
    width: 1024px;
    position: absolute;
    top: -2000px;
    left: -2000px;
}
...
```