[PDF-LIB][Electron][TypeScript] Edit PDF
Tags: typescript, electron, pdflib
Published: 2021-04-25 02:00:17

## Intro
This time, I will try editing PDF files by PDF-LIB.

* [PDF-LIB · Create and modify PDF documents in any JavaScript environment.](https://pdf-lib.js.org/)

### Environments
* Node.js ver.16.0.0
* TypeScript ver.4.2.4
* Electron ver.12.0.2
* Webpack ver.5.31.2
* pdfjs-dist ver.2.7.570
* dpi-tools ver.1.0.7
* pdf-lib ver.1.16.0

## Call main process from renderer process
I tried calling the main process from the renderer process before.

* [[TypeScript] Try Electron](https://dev.to/masanori_msl/typescript-try-electron-43a0)

At that time, I used DOM events.
But because I also want to call method to send some data to main process, I will try using "contextBridge" in this time.

* [Context Isolation | Electron](https://www.electronjs.org/docs/tutorial/context-isolation)
* [contextBridge | Electron](https://www.electronjs.org/docs/api/context-bridge)

To use contextBridge. I declare an API in "preload.ts" and add event handlers in the main process.

#### preload.ts
```ts
import { ipcRenderer, contextBridge } from 'electron';
contextBridge.exposeInMainWorld('myapi', {
    greet: async (data: any) => await ipcRenderer.invoke('greet', data),
    saveFile: async (name: string, data: Uint8Array) => await ipcRenderer.invoke('saveFile', name, data)
  }
)
```

#### main.ts
```ts
import { app, ipcMain, BrowserWindow } from 'electron';
import * as path from 'path';
import { FileSaver } from './files/fileSaver';
...
ipcMain.handle('greet', (_, data) => {
  console.log(`Hello ${data}`);
});
ipcMain.handle('saveFile', async (_, name: string, data: Uint8Array) => {
  const buffer = Buffer.from(data);
  const result = await fileSaver.saveFileAsync(name, buffer);
  if(result.succeeded === true) {
    console.log("OK");
  } else {
    console.error(result.errorMessage);
  }
});
```

Now I can call the API mothods.

```ts
export function callSample() {
    window.myapi.greet("hello");
}
```

### Add type declaration
But I got an error.
Because "window" hasn't had the API declaration.

#### types/global.d.ts
```ts
declare global {
    interface Window {
        myapi: Sandbox
    };
}
export interface Sandbox {
    greet: (message: string) => void,
    saveFile: (name: string, data: Uint8Array) => void
};
```

* [TypeScript: Documentation - Global .d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html)
* [global.d.ts - TypeScript Deep Dive](https://basarat.gitbook.io/typescript/project/modules/globals)
* [lib.d.ts - TypeScript Deep Dive](https://basarat.gitbook.io/typescript/type-system/lib.d.ts)
* [Electron + TypeScript でセキュリティを気にしてみる - Qiita](https://qiita.com/sprout2000/items/5253a8dee40197359949)

## Edit PDF
Next, I will try editing PDF files.
Creating textfields or images, getting the fields informations, and so on.

I use [the sample code](https://pdf-lib.js.org/#modify-document) to try.

#### pdfEditor.ts
```ts
import { degrees, PDFDocument, rgb, StandardFonts } from "pdf-lib";

export class PdfEditor {
    public async edit(filePath: string) {
        // load PDF file from local file path
        const existingPdfBytes = await loadFile(filePath);
        const pdfDoc = await PDFDocument.load(existingPdfBytes);
        const firstPage = pdfDoc.getPages()[0];

        // create a text field
        const form = pdfDoc.getForm();
        const textField = form.createTextField('SampleField');
        textField.setText('Hello');

        textField.addToPage(firstPage, {
            x: 50,
            y: 75,
            width: 200,
            height: 100,
            textColor: rgb(1, 0, 0),
            backgroundColor: rgb(0, 1, 0),
            borderColor: rgb(0, 0, 1),
            borderWidth: 2,
            rotate: degrees(90),
            font: await pdfDoc.embedFont(StandardFonts.Helvetica),
        });
        const saveData = await pdfDoc.save();
        window.myapi.saveFile('sample.pdf', saveData);
    }
    private async loadFile(path: string): Promise<ArrayBuffer> {
        return fetch(path).then(res => res.arrayBuffer());
    }
}
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eepo64grajqd390knoln.png)


### Get the textfield information
About PDFTextField, I could get and edit texts.

#### pdfEditor.ts
```ts
...
export class PdfEditor {
    public async edit(filePath: string) {
...
        const sampleField = form.getTextField('SampleField');
        // get infomations
        console.log(sampleField.getName());
        console.log(sampleField.getText());
        // update text
        sampleField.setText('World');
        sampleField.updateAppearances(await pdfDoc.embedFont(StandardFonts.TimesRomanBoldItalic));
        const saveData = await pdfDoc.save();
        window.myapi.saveFile('sample.pdf', saveData);
    }
}
```

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

But I couldn't find any methods for getting or editing the field. 
So I gave up resizing, moving or removing the field.

### Set image
PDFTextField also can set an image.
The image size is scaled for fitting the short side.

#### pdfEditor.ts
```ts
...
        const sampleField = form.getTextField('SampleField');
...
        // update text
        const image = await pdfDoc.embedPng(await this.loadFile(imageFilePath));
        sampleField.setImage(image);
        const saveData = await pdfDoc.save();
        window.myapi.saveFile('sample2.pdf', saveData);
...
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bw6ud0hmpmzh0bj2l0qm.png)

### Set edited image
I try adding spaces around the image first.
After that, I add it into the PDF.

#### pdfEditor.ts
```ts
...
        const sampleField = form.getTextField('SampleField');
...
        // update text
        const editedImageData = await this.addSpace(await this.loadFileBlob(imageFilePath), 300, 100);
        const image = await pdfDoc.embedPng(editedImageData);
        sampleField.setImage(image);
        const saveData = await pdfDoc.save();
        window.myapi.saveFile('sample2.pdf', saveData);
...
    private async loadFileBlob(path: string): Promise<Blob> {
        return fetch(path).then(res => res.blob());
    }
    private async addSpace(fileData: Blob, horizontalSpace: number, verticalSpace: number): Promise<Uint8Array> {
        return new Promise((resolve) => {
            const newImage = new Image();
            newImage.onload = _ => {
                const canvas = document.createElement('canvas');
                const canvasHeight = newImage.height + verticalSpace;
                const canvasWidth = newImage.width + horizontalSpace;
                canvas.height = canvasHeight;
                canvas.width = canvasWidth;
                const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
                ctx.fillStyle = "rgb(128,255,128)";
                ctx.fillRect(0, 0, canvasWidth, canvasHeight);
                ctx.drawImage(newImage, 0, 0, canvasWidth, canvasHeight,
                    (horizontalSpace / 2), (verticalSpace / 2), canvasWidth, canvasHeight);
                canvas.toBlob(async (blob) => {
                    resolve(await generateImageData(blob!));
                });
            };
            newImage.src = URL.createObjectURL(fileData);
        });
    }
}
```
#### imageEditor.ts
```ts
...
import * as dpiTools from 'dpi-tools';
...
export async function generateImageData(imageData: Blob): Promise<Uint8Array> {
    return new Promise(async (resolve) => {
        const updatedBlob = await dpiTools.changeDpiBlob(imageData, 300);
        const fileReader = new FileReader();
        fileReader.onload = async (ev) => {
            resolve(new Uint8Array(fileReader.result as ArrayBuffer));
        };
        fileReader.readAsArrayBuffer(updatedBlob);
    });
}
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cweup2yu4dky92rmly6m.png)

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

### Use edited PDF with PDF.js
Because PDF-LIB save data type is Uint8Array and PDF.js can get PDF data from Uint8Array.
So all I need to do is just return PDF-LIB saved data and set them as the arguments of "pdf.getDocument".

#### pdfEditor.ts
```ts
    public async edit(filePath: string, imageFilePath: string): Promise<Uint8Array> {
        const existingPdfBytes = await this.loadFile(filePath);
        const pdfDoc = await PDFDocument.load(existingPdfBytes);
...
        // return Uint8Array
        return await pdfDoc.save();        
    }
...
```
#### imageEditor.ts
```ts
...
export class ImageEditor {
...
    public async loadDocument(fileData: Uint8Array): Promise<number> {           
        this.pdfDocument = await pdf.getDocument(fileData).promise;
        if(this.pdfDocument == null) {
            console.error('failed loading document');
            return 0;
        }
        return this.pdfDocument.numPages;
    }
...
```
#### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s5g2ro5focv5lj11rf6w.png)