[TypeScript] 이미지 편집

45659 단어 typescript

소개





  • 이번에는 고해상도 이미지를 저장하고 이미지를 편집해보도록 하겠습니다.
  • 전자 ver.12.0.2
  • 타이프스크립트 ver.4.2.4
  • 웹팩 ver.5.31.2
  • webpack-cli ver.4.6.0
  • pdfjs-dist 버전 2.7.570
  • dpi-tools ver.1.0.7

  • 이미지를 고해상도로 저장



    지난번에는 PDF에서 이미지를 가져와서 저장했습니다.
    그러나 그들은 96 DPI로 저장되었습니다.

    Canvas.toBlob()이 96 DPI로 설정되었기 때문입니다.
  • HTMLCanvasElement.toBlob() - Web APIs | MDN

  • 더 높은 해상도로 저장하기 위해 "dpi-tools"를 사용했습니다.
  • GitHub - insomnia-dev/dpi-tools: Javascript library that allows you to change an image's DPI settings

  • d.ts



    TypeScript 코드에서 "dpi-tools"를 사용하려고 했을 때 문제가 발생했습니다.
    Type Declaration 파일이 없었기 때문에 가져올 수 없었습니다.


    그래서 Type Declaration 파일을 직접 추가했습니다.

    먼저 아래와 같은 파일을 만들었습니다.

    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;
    

    그리고 "node_modules/@types/dpi-tools"디렉토리에 넣었습니다.

    이제 "dpi-tools"를 가져올 수 있습니다 :)

    이미지 저장


    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);
    }
    

    결과




  • TypeScript: Documentation - Declaration Reference
  • Writing Declaration Files for @types | TypeScript

  • 사용자 지정 d.ts 파일 저장



    업데이트 2021-04-21
    node_modules에 index.d.ts 파일이 있으면 안 됩니다.

    npm으로 다른 패키지를 설치하면 사용자 지정 d.ts 파일이 삭제되기 때문입니다.

    그래서 나는 그것들을 내 프로젝트에서 만든 "types"폴더로 옮겼습니다.

    그리고 tsconfig.js를 변경했습니다.

    tsconfig.js



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

  • TypeScript: Documentation - Module Resolution

  • 직사각형 그리기 및 이미지 자르기


  • 캔버스에 직사각형 그리기
  • PDF에서 가져온 이미지를 직사각형 크기와 동일하게 자르기
  • 잘라낸 이미지 저장

  • 1. 캔버스에 사각형 그리기



    "fillRect"로 사각형을 그릴 수 있습니다.

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


    그리고 "clearRect"로 제거할 수도 있습니다.

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


    한 가지 문제는 PDF 이미지와 사각형을 구분하지 않는다는 것입니다.
    그래서 "fillRect"와 "clearRect"를 여러번 실행하면 이런 이미지가 됩니다.


    따라서 사각형을 그리기 위해 다른 캔버스를 추가합니다.

    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>
    


    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;
    }
    


    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});        
        }
    }
    


    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;
        }
    }
    


    결과





    PDF에서 가져온 이미지를 직사각형 크기와 동일하게 자르기



    캔버스 이미지를 직접 자를 수 없기 때문입니다.
    이미지를 만들고 자르기 위해 "drawImage"를 사용해야 합니다.

    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);
        }
    }
    


    결과




    전에




    후에


  • CanvasRenderingContext2D.drawImage() - Web APIs | MDN

  • 3. 자른 이미지 저장



    자른 이미지도 캔버스에 그려지기 때문입니다.
    그래서 나는 그것을 .

    좋은 웹페이지 즐겨찾기