[Electron][TypeScript][PDF.js] Create images from PDF

masanori_msl

Masui Masanori

Posted on April 14, 2021

[Electron][TypeScript][PDF.js] Create images from PDF

Intro

This time, I will try printing PDF files with the Electron application.
Before printing, I want to get images from the PDF file.

Environments

  • electron ver.12.0.2
  • typescript ver.4.2.4
  • webpack ver.5.31.2
  • webpack-cli ver.4.6.0
  • pdfjs-dist ver.2.7.570

Use Webpack in client-side

Because I want to use Webpack in client-side as same as when I create ASP.NET Core applications, I add Webpack and bundle the client-side TypeScript code.

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        'main.page': './clients/main.page.ts',
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'js/clients'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};
Enter fullscreen mode Exit fullscreen mode

And I got a script error.

Uncaught EvalError: Refused to evaluate a string as JavaScript 
because 'unsafe-eval' is not an allowed source of script 
in the following Content Security Policy directive: "default-src 'self'"
Enter fullscreen mode Exit fullscreen mode

This error came from "Content-Security-Policy" for electron and bundled script for development use "eval()".

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
...
Enter fullscreen mode Exit fullscreen mode

main.page.js

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
...
/***/ ((__unused_webpack_module, exports) => {

eval("\r\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\r\nexports.greet = void 0;\r\nfunction greet(message) {\r\n    alert(message);\r\n    return 'OK';\r\n}\r\nexports.greet = greet;\r\n\n\n//# sourceURL=webpack://Page/./clients/main.page.ts?");

/***/ })

/******/    });
...
Enter fullscreen mode Exit fullscreen mode

I think the easiest way to resolve is change mode of Webpack to "production".

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'production',
    entry: {
        'main.page': './clients/main.page.ts',
    },
...
Enter fullscreen mode Exit fullscreen mode

Get images from PDF files

To get images from PDF files, I will use PDF.js.

First, I drew the first page into a canvas.

main.ts

...
async function createWindow () {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });

  win.loadFile('./views/index.html');
  const filePath = `C:/Users/example/sample600.pdf`;
  win.webContents.executeJavaScript(`Page.load('${filePath}')`)
    .catch(error => console.error(error));
}
...
Enter fullscreen mode Exit fullscreen mode

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body style="background: white;">
    <div>
        <div id="area"></div>
        <button id="save_button">Click</button>
        <div id="result"></div>
    </div>
    <script src="../js/clients/main.page.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

main.page.ts

import * as pdf from 'pdfjs-dist';
let canvas: HTMLCanvasElement;

export async function load(filePath: string) {

    const rootElement = document.getElementById('area') as HTMLElement;
    pdf.GlobalWorkerOptions.workerSrc =
        '../node_modules/pdfjs-dist/build/pdf.worker.js';

    const pdfDocument = await pdf.getDocument(filePath).promise;

    // Request a first page
    const pdfPage = await pdfDocument.getPage(1);
    // Display page on the existing canvas with 100% scale.
    const viewport = pdfPage.getViewport({ scale: 1.0, rotation: 0 });
    canvas = document.createElement('canvas');
    canvas.id = 'sample_page';

    canvas.width = viewport.width;
    canvas.height = viewport.height;

    const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

    await pdfPage.render({
      canvasContext: ctx,
      viewport
    }).promise;
    rootElement.appendChild(canvas);    
}
Enter fullscreen mode Exit fullscreen mode

Result

Alt Text

Resolution

Now I have a problem.
The created image size was { width:1190, height: 841 }.
Its DPI was 72.

How can I change to high resolution?

According to the document, I should change the scale of ViewPort.
Because I don't know the natural page size of PDF files, so I convert from pixel size of 72DPI to millimeter, after that I convert to pixel size of 300DPI.

sizeConverter.ts

export class SizeConverter {
    public static ConvertFromMmToPx(sizeMm: number, dpi: number): number {
        if(sizeMm <= 0 || dpi <= 0) {
            return 0;
        }
        const sizeInch = sizeMm / 25.4;
        return Math.round(sizeInch * dpi);
    }
    public static ConvertFromPxToMm(sizePx: number, dpi: number): number {
        if(sizePx <= 0 || dpi <= 0) {
            return 0;
        }
        const sizeInch = sizePx / dpi;
        return Math.round(sizeInch * 25.4);
    }
}
Enter fullscreen mode Exit fullscreen mode

main.page.ts

import { SizeConverter } from './sizeConverter';
...
export async function load(filePath: string) {
...
    const pdfDocument = await pdf.getDocument(filePath).promise;

    // Request a first page
    const pdfPage = await pdfDocument.getPage(1);
    // Display page on the existing canvas with 100% scale.
    const viewport = pdfPage.getViewport({ scale: 1.0, rotation: 0 });
    canvas = document.createElement('canvas');
    canvas.id = 'sample_page';

    const horizontalMm = SizeConverter.ConvertFromPxToMm(viewport.width, 72);
    const verticalMm = SizeConverter.ConvertFromPxToMm(viewport.height, 72);

    const actualWidth = SizeConverter.ConvertFromMmToPx(horizontalMm, 300);
    const actualHeight = SizeConverter.ConvertFromMmToPx(verticalMm, 300);

    canvas.width = actualWidth;
    canvas.height = actualHeight;
    canvas.style.width = `${viewport.width}px`;
    canvas.style.height = `${viewport.height}px`;

    const scale = Math.min(actualWidth / viewport.width, actualHeight / viewport.height);
    const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;

    await pdfPage.render({
      canvasContext: ctx,
      viewport: pdfPage.getViewport({ scale: scale, rotation: 0 }),
    }).promise;
    rootElement.appendChild(canvas);   
}
Enter fullscreen mode Exit fullscreen mode

Save images

To save file silently, I send Blob data to electron-side.
Because I can't use "Blob" in electron-side, so I have to convert Blob to Buffer before sending to electron-side.

preload.ts

import { ipcRenderer } from 'electron';
window.addEventListener('DOMContentLoaded', () => {
    const button = document.getElementById('save_button');
    if(button != null) {
        button.addEventListener('click', (ev) => {
          saveFile(); 
        });
    }
  });
function saveFile() {
  const canvas = document.getElementById('sample_page') as HTMLCanvasElement;
  canvas.toBlob((blob) => {
    const fileReader = new FileReader();
    fileReader.onload = async (ev) => {
      const buffer = Buffer.from(fileReader.result as ArrayBuffer);
      ipcRenderer.send('save_file', "sample.png", buffer);
    };
    fileReader.readAsArrayBuffer(blob!);
  }, 'image/png', 1);
}
Enter fullscreen mode Exit fullscreen mode

main.ts

...
import * as fs from 'fs';
...
ipcMain.on('save_file', (_, fileName, buffer) => {
  // get ArrayBuffer
  fs.writeFile(fileName, buffer, error => {
    if(error) {
      console.error(error);
    }
    else {
      console.log("OK");
    }
  });
});
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
masanori_msl
Masui Masanori

Posted on April 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related