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)
}
)
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);
}
});
Now I can call the API mothods.
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
declare global {
interface Window {
myapi: Sandbox
};
}
export interface Sandbox {
greet: (message: string) => void,
saveFile: (name: string, data: Uint8Array) => void
};
- TypeScript: Documentation - Global .d.ts
- global.d.ts - TypeScript Deep Dive
- lib.d.ts - TypeScript Deep Dive
- Electron + TypeScript でセキュリティを気にしてみる - Qiita
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());
}
}
Result
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);
}
}
Result
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);
...
Result
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);
});
}
}
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);
});
}
Result
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();
}
...
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;
}
...
Top comments (0)