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)