DEV Community

Masui Masanori
Masui Masanori

Posted on

[PDF-LIB][Electron][TypeScript] Edit PDF

Intro

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

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.

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.

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

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

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

Now I can call the API mothods.

export function callSample() {
    window.myapi.greet("hello");
}
Enter fullscreen mode Exit fullscreen mode

Add type declaration

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

types/global.d.ts

declare global {
    interface Window {
        myapi: Sandbox
    };
}
export interface Sandbox {
    greet: (message: string) => void,
    saveFile: (name: string, data: Uint8Array) => void
};
Enter fullscreen mode Exit fullscreen mode

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 to try.

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

Result

Alt Text

Get the textfield information

About PDFTextField, I could get and edit texts.

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

Result

Alt Text

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

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

Result

Alt Text

Set edited image

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

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

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

Result

Alt Text

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

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

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

Result

Alt Text

Top comments (0)