[TypeScript] Edit images
Tags: typescript
Published: 2021-04-17 08:36:04

## Intro
* [[TypeScript] Try Electron](https://dev.to/masanori_msl/typescript-try-electron-43a0)
* [[Electron][TypeScript][PDF.js] Create images from PDF](https://dev.to/masanori_msl/electron-typescript-pdf-js-create-images-from-pdf-55af)

This time, I will try saving high resolution images and editing images.

* electron ver.12.0.2
* typescript ver.4.2.4
* webpack ver.5.31.2
* webpack-cli ver.4.6.0
* pdfjs-dist ver.2.7.570
* dpi-tools ver.1.0.7

## Save images as high resolution
Last time, I got images from PDF and saved them.
But they were saved as 96 DPI.

Because Canvas.toBlob() set as 96 DPI.

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

To save as more higher resolution, I used "dpi-tools".

* [GitHub - insomnia-dev/dpi-tools: Javascript library that allows you to change an image's DPI settings](https://github.com/insomnia-dev/dpi-tools)

### d.ts
When I tried using "dpi-tools" in TypeScript code, I got a problem.
Because it hadn't had Type Declaration files, I couldn't import.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19sdsovcs6buhy7rrfwf.jpg)

So I added a Type Declaration file by myself.

First, I created a file like below.

#### index.d.ts
```ts
export function changeDpiDataUrl(base64Image: string, dpi: number): Array<string>;
export function changeDpiBlob(blob: Blob, dpi: number): Promise<Blob>;
export function changeDpiBuffer(buffer: Buffer, dpi: number): Buffer;
```

And I put it into a directory of "node_modules/@types/dpi-tools".

Now I can import "dpi-tools" :)

### Save images 
#### preload.ts
```ts
import * as dpiTools from 'dpi-tools';
...
function saveFile() {
    const canvas = document.getElementById('sample_page') as HTMLCanvasElement;
    canvas.toBlob(async (blob) => {
        // save as 300 DPI
        const updatedBlob = await dpiTools.changeDpiBlob(blob!, 300);
        const fileReader = new FileReader();
        fileReader.onload = async (ev) => {
            const buffer = Buffer.from(fileReader.result as ArrayBuffer);
            ipcRenderer.send('save_file', "sample.jpg", buffer);
        };
        fileReader.readAsArrayBuffer(updatedBlob);
    }, 'image/jpeg', 1);
}
```

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

* [TypeScript: Documentation - Declaration Reference](https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html)
* [Writing Declaration Files for @types | TypeScript](https://devblogs.microsoft.com/typescript/writing-dts-files-for-types/)

### Save custom d.ts files
<font color='red'><b>update 2021-04-21</b></font>
I shouldn't the index.d.ts file in node_modules.

Because when I install another package by npm, my custom d.ts files would be deleted.

So I moved them into "types" folder what I had created in my project.

And I changed tsconfig.js.

#### tsconfig.js
```js
...
    "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
    "paths": {
      "*": ["*", "types/*"]
    },
...
```
* [TypeScript: Documentation - Module Resolution](https://www.typescriptlang.org/docs/handbook/module-resolution.html)

## draw rectangle and crop images
1. Draw a rectangle on Canvas
2. Crop the image what is gotten from PDF as same as the rectangle size
3. Save cropped image

### 1. Draw a rectangle on Canvas
I can draw rectangles by "fillRect".

```ts
const ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(300, 50, 3000, 500);
```

And I also can remove them by "clearRect".

```ts
const ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.clearRect(300, 50, 3000, 500);
```

One problem is they don't distinguish between the PDF image and the rectangle.
So if I execute "fillRect" and "clearRect" several time, the image will be like this.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5cj2fj9y23bboh7fdcc.jpg)

Thus, I add another canvas to draw rectangles.

#### index.html
```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <link rel="stylesheet" href="../css/main.page.css" />
</head>
<body style="background: white;">
    <button id="change_page">Change</button>
    <div>
        <button id="save_button">Click</button>
        <button onclick="Page.crop()">Crop</button>
        <div id="area">
            <div class="draw_canvas_area">
                <canvas id="image_canvas"></canvas>
                <canvas id="cropbox_canvas"></canvas>
            </div>
            <div id="cropped_image_area"><canvas id="cropped_image_canvas"></canvas></div>
        </div>
    </div>
    <script src="../js/clients/main.page.js"></script>
</body>
</html>
```
#### main.page.css
```css
.draw_canvas_area{
    border: 1px black solid;
    position: absolute;
    height: 90vh;
    width: 90vw;
    overflow: auto;
}
.draw_canvas_area canvas {
    position: absolute;
}
#cropped_image_area {
    display: none;
}
```
#### main.page.ts
```ts
import { ImageEditor } from "./imageEditor";

let imageEditor = new ImageEditor();
...
export async function load(filePath: string) {
    const pageCount = await imageEditor.loadDocument(filePath);
    if(pageCount > 0) {
        await imageEditor.loadPage(1);
        // draw a rectangle for two times
        imageEditor.drawRectangle({x: 300, y: 50, width: 3000, height: 500});
        imageEditor.drawRectangle({x: 400, y: 500, width: 200, height: 1200});        
    }
}
```
#### imageEditor.ts
```ts
import * as pdf from 'pdfjs-dist';
import { SizeConverter } from './sizeConverter';
import { PDFDocumentProxy } from 'pdfjs-dist/types/display/api';

type Rect = {
    x: number,
    y: number,
    width: number,
    height: number
};
const defaultCanvasDpi = 72;
export class ImageEditor {
    private imageCanvas: HTMLCanvasElement;
    private rectangleCanvas: HTMLCanvasElement;
    private pdfDocument: PDFDocumentProxy|null = null;
    private lastRectangle: Rect|null = null;
    public constructor() {
        this.imageCanvas = document.getElementById('image_canvas') as HTMLCanvasElement;
        this.rectangleCanvas = document.getElementById('cropbox_canvas') as HTMLCanvasElement;
    }
    public async loadDocument(filePath: string): Promise<number> {
        pdf.GlobalWorkerOptions.workerSrc =
            '../node_modules/pdfjs-dist/build/pdf.worker.js';
        this.pdfDocument = await pdf.getDocument(filePath).promise;
        if(this.pdfDocument == null) {
            console.error('failed loading document');
            return 0;
        }
        return this.pdfDocument.numPages;
    }
    public async loadPage(pageNumber: number): Promise<boolean> {
        if(this.pdfDocument == null) {
            return false;
        }
        const pdfPage = await this.pdfDocument.getPage(pageNumber);
        if(pdfPage == null) {
            return false;
        }
        // Display page on the existing canvas with 100% scale.
        const viewport = pdfPage.getViewport({ scale: 1.0, rotation: 0 });
        
        const horizontalMm = SizeConverter.ConvertFromPxToMm(viewport.width, defaultCanvasDpi);
        const verticalMm = SizeConverter.ConvertFromPxToMm(viewport.height, defaultCanvasDpi);

        const actualWidth = SizeConverter.ConvertFromMmToPx(horizontalMm, 300);
        const actualHeight = SizeConverter.ConvertFromMmToPx(verticalMm, 300);

        this.imageCanvas.width = actualWidth;
        this.imageCanvas.height = actualHeight;
        this.imageCanvas.style.width = `${viewport.width}px`;
        this.imageCanvas.style.height = `${viewport.height}px`;

        this.rectangleCanvas.width = actualWidth;
        this.rectangleCanvas.height = actualHeight;
        this.rectangleCanvas.style.width = `${viewport.width}px`;
        this.rectangleCanvas.style.height = `${viewport.height}px`;

        const scale = Math.min(actualWidth / viewport.width, actualHeight / viewport.height);
        const ctx = this.imageCanvas.getContext("2d") as CanvasRenderingContext2D;
        
        await pdfPage.render({
            canvasContext: ctx,
            viewport: pdfPage.getViewport({ scale: scale, rotation: 0 }),
        }).promise;
        return true;
    }
    public drawRectangle(rect: Rect) {
        this.clearRectangle();
        const ctx = this.rectangleCanvas.getContext("2d") as CanvasRenderingContext2D;
        ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
        ctx.fillRect(rect.x, rect.y,
            rect.width, rect.height);
        this.lastRectangle = rect;
    }
    public clearRectangle() {        
        if(this.lastRectangle == null) {
            return;
        }
        const ctx = this.rectangleCanvas.getContext("2d") as CanvasRenderingContext2D;
        ctx.clearRect(this.lastRectangle.x, this.lastRectangle.y,
            this.lastRectangle.width, this.lastRectangle.height);
        this.lastRectangle = null;
    }
}
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y89e71pde704g53tarnl.jpg)

### Crop the image what is gotten from PDF as same as the rectangle size
Because I can't crop Canvas image directly.
I have to create an image and use "drawImage" to crop.

#### imageEditor.ts
```ts
...
export class ImageEditor {
    private imageCanvas: HTMLCanvasElement;
    private rectangleCanvas: HTMLCanvasElement;
    private croppedImageCanvas: HTMLCanvasElement;
    private pdfDocument: PDFDocumentProxy|null = null;
    private lastRectangle: Rect|null = null;
    public constructor() {
...
        this.croppedImageCanvas = document.getElementById('cropped_image_canvas') as HTMLCanvasElement;
    }
...
    public crop() {
        if(this.lastRectangle == null) {
            console.error("no rectangle");
            return;
        }
        // Convert Canvas image to Image 
        const newImage = new Image();
        newImage.onload = _ => {
            if(this.lastRectangle == null) {
                console.error("no rectangle");                    
                return;
            }
            // scale Canvas display size
            const horizontalMm = SizeConverter.ConvertFromPxToMm(this.lastRectangle.width, 300);
            const verticalMm = SizeConverter.ConvertFromPxToMm(this.lastRectangle.height, 300);
            const scaledWidth = SizeConverter.ConvertFromMmToPx(horizontalMm, defaultCanvasDpi);
            const scaledHeight = SizeConverter.ConvertFromMmToPx(verticalMm, defaultCanvasDpi);
            this.croppedImageCanvas.width = this.lastRectangle.width ;
            this.croppedImageCanvas.height = this.lastRectangle.height;
            this.croppedImageCanvas.style.width = `${scaledWidth}px`;
            this.croppedImageCanvas.style.height = `${scaledHeight}px`;

            const ctx = this.croppedImageCanvas.getContext("2d") as CanvasRenderingContext2D;     
            // in "drawImage", I can't use scaled values because Canvas has already scaled.
            ctx.drawImage(newImage, 0, 0, this.imageCanvas.width, this.imageCanvas.height,
                -this.lastRectangle.x, -this.lastRectangle.y, this.imageCanvas.width, this.imageCanvas.height);
        };
        newImage.src = this.imageCanvas.toDataURL('image/png', 1);
    }
}
```
#### Result
##### Before
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gvk5itbd0hmfqryz2ixu.jpg)

##### After
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/heq3heu93fh6qehfviub.jpg)

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

### 3. Save cropped image
Because the cropped image also drawn on a Canvas.
So I can save it as same as [last time](https://dev.to/masanori_msl/electron-typescript-pdf-js-create-images-from-pdf-55af).