DEV Community

loading...

[TypeScript] Edit images

masanori_msl profile image Masui Masanori Updated on ・6 min read

Intro

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.

To save as more higher resolution, I used "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

So I added a Type Declaration file by myself.

First, I created a file like below.

index.d.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;
Enter fullscreen mode Exit fullscreen mode

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

Now I can import "dpi-tools" :)

Save images

preload.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);
}
Enter fullscreen mode Exit fullscreen mode

Result

Alt Text

Save custom d.ts files

update 2021-04-21
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

...
    "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
    "paths": {
      "*": ["*", "types/*"]
    },
...
Enter fullscreen mode Exit fullscreen mode

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".

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

And I also can remove them by "clearRect".

const ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.clearRect(300, 50, 3000, 500);
Enter fullscreen mode Exit fullscreen mode

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

Thus, I add another canvas to draw rectangles.

index.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>
Enter fullscreen mode Exit fullscreen mode

main.page.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;
}
Enter fullscreen mode Exit fullscreen mode

main.page.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});        
    }
}
Enter fullscreen mode Exit fullscreen mode

imageEditor.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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Result

Alt Text

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

...
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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Result

Before

Alt Text

After

Alt Text

3. Save cropped image

Because the cropped image also drawn on a Canvas.
So I can save it as same as last time.

Discussion (0)

pic
Editor guide