DEV Community

Masui Masanori
Masui Masanori

Posted on

6 1

[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

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay