Printing a PDF file with Electron JS

2020-05-22 05:09发布

问题:

I am trying to create an Electron JS app that has the purpose to print letter size PDFs.

This is my snippet of code for printing:

win = new BrowserWindow({
  width: 378, 
  height: 566, 
  show: true, 
  webPreferences: {
    webSecurity: false,
    plugins: true
  }
});

// load PDF
win.loadURL('file://' + __dirname + '/header1_X_BTR.pdf');

// if pdf is loaded start printing
win.webContents.on('did-finish-load', () => {
  win.webContents.print({silent: true, printBackground:true});
});

My issues are: if I have print({silent:true}) my printer prints an empty page. If I have print({silent:false}), the printer prints in the same way as the screenshot, with headers, controls, etc.

I need a silent print of the PDF content, and I can't manage to do it for days. Did anyone experience the same thing with Electron?

回答1:

If you have already have the pdf file or you save the pdf before printing "I assuming it is", then you can grab the file location then you can use externals process to do the printing using child_process.

You can use lp command or PDFtoPrinter for windows

const ch = require('os');

switch (process.platform) {
    case 'darwin':
    case 'linux':
        ch.exec(
            'lp ' + pdf.filename, (e) => {
                if (e) {
                    throw e;
                }
            });
        break;
    case 'win32':
        ch.exec(
            'ptp ' + pdf.filename, {
                windowsHide: true
            }, (e) => {
                if (e) {
                    throw e;
                }
            });
        break;
    default:
        throw new Error(
            'Platform not supported.'
        );
}

I hope it helps.

Edit: You can also use SumatraPDF for windows https://github.com/sumatrapdfreader/sumatrapdf



回答2:

The easiest way to do this is to render the PDF pages to individual canvas elements on a page using PDF.js and then call print.

I fixed this gist to use the PDF.js version (v1) it was designed for and its probably a good starting point.

This is essentially what the electron/chrome pdf viewer is doing but now you have full control over the layout!

<html>
<body>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/1.10.90/pdf.js"></script>
<script type="text/javascript">
function renderPDF(url, canvasContainer, options) {
    var options = options || { scale: 1 };
        
    function renderPage(page) {
        var viewport = page.getViewport(options.scale);
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var renderContext = {
          canvasContext: ctx,
          viewport: viewport
        };
        
        canvas.height = viewport.height;
        canvas.width = viewport.width;
        canvasContainer.appendChild(canvas);
        
        page.render(renderContext);
    }
    
    function renderPages(pdfDoc) {
        for(var num = 1; num <= pdfDoc.numPages; num++)
            pdfDoc.getPage(num).then(renderPage);
    }
    PDFJS.disableWorker = true;
    PDFJS.getDocument(url).then(renderPages);
}   
</script> 

<div id="holder"></div>

<script type="text/javascript">
renderPDF('//cdn.mozilla.net/pdfjs/helloworld.pdf', document.getElementById('holder'));
</script>  

</body>
</html>



回答3:

I'm facing the same issue. It appears the PDF printing to a printer is just not implemented in Electron, despite it's been requested since 2017. Here is another related question on SO and the feature request on GitHub:

  • Silent printing in electron
  • Support printing in native PDF rendering

One possible solution might be to use Google PDFium and a wrapping NodeJS library which appears to allow conversion from PDF to a set of EMFs, so the EMFs can be printed to a local/network printer, at least on Windows.

As another viable option, this answer provides a simple C# solution for PDF printing using PdfiumViewer, which is a PDFium wrapper library for .NET.

I'm sill looking at any other options. Utilizing a locally installed instance of Acrobat Reader for printing is not an acceptable solution for us.


UPDATED. For now, PDF.js solves the problem with rendering/previewing individual pages, but as to printing itself, it appears Electron (at the time of this posting) just lacks the proper printing APIs. E.g., you can't set paper size/landscape portrait mode etc. Moreover, when printing, PDF.js produces rasterized printouts - thanks to how HTML5 canvas work - unlike how Chrome PDF Viewer does it. Here is a discussion of some other PDF.js shortcomings.

So for now I think we might go on with a combination of PDF.js (for UI in the Electron's Renderer process) and PDFium (for actual printing from the Main process).

Based on Tim's answer, here's a version of the PDF.js renderer using ES8 async/await (supported as of the current version of Electron):

async function renderPDF(url, canvasContainer, options) {
    options = options || { scale: 1 };

    async function renderPage(page) {
        let viewport = page.getViewport(options.scale);
        let canvas = document.createElement('canvas');
        let ctx = canvas.getContext('2d');
        let renderContext = {
            canvasContext: ctx,
            viewport: viewport
        };

        canvas.height = viewport.height;
        canvas.width = viewport.width;
        canvasContainer.appendChild(canvas);

        await page.render(renderContext);
    }

    let pdfDoc = await pdfjsLib.getDocument(url);

    for (let num = 1; num <= pdfDoc.numPages; num++)
    {
        if (num > 1)
        {
            // page separator
            canvasContainer.appendChild(document.createElement('hr'));
        }
        let page = await pdfDoc.getPage(num);
        await renderPage(page);
    }
}


回答4:

Since your are using contents.print([options], [callback]) I will assume that you want to print on paper and not on your Disk.


The answer to your issue is simple. It is the event you are listening on which is causing the error. So if you simply do this:

  winObject.webContents.on('did-frame-finish-load', () => {
    setTimeout(() => {winObject.webContents.print({silent: true, printBackground:true})}, 3000);
  });

everything will work fine if the default printer is the right one. I did test this and it will do its job more or less. You can change my event to whatever event you like, the important part is the waiting with setTimeout. The PDF you are trying to print is simply not available in the frame when using silent:true.

However let me get into detail here a little bit to make things clear:

Electron will load Files or URLs into a created window (BrowserWindow) which is bound to events. The problem is that every event "can" behave differently on different systems. We have to live with that and cannot change this easily. But knowing this will help improve the development of custom Apps.

If you load urls or htmls everything will work without setting any custom options. Using PDFs as source we have to use this:

import electron, { BrowserWindow } from 'electron';
const win = new BrowserWindow({
  // @NOTE I did keep the standard options out of this.
  webPreferences: { // You need this options to load pdfs
    plugins: true // this will enable you to use pdfs as source and not just download it.
  }
});

hint: without webPreferences: { plugins: true } your source PDF will be downloaded instead of loaded into the window.

That said you will load your PDF into the webContents of your window. So we have to listen on events compatible with BrowserWindow. You did everything right, the only part you missed was that printing is another interface.

Printing will capture your webContents as it is when you press "print". This is very inportant to know when working with printers. Because if something will load slightly longer on a different system, for example the PDFs viewer will be still dark grey without the letters, then your printing will print the dark grey background or even the buttons.

That little issue is easily fixed with setTimeout().

Useful Q&A for printing with electron:

  • Silent printing in electron
  • Print: How to stick footer on every page to the bottom?

However there are alot more possible issues with printing, since most of the code is behind closed doors without worldwide APIs to use. Just keep in mind that every printer can behave differently so testing on more machines will help.



回答5:

So it seems like you're trying to download the pdf file rather than print a pdf of the current screen which is what print tries to do. As such, you have a couple of options.

1) Disable the native pdf viewer in electron:

If you don't care about the electron window displaying the pdf, disabling the native pdf viewer in electron should instead cause it to treat the file as a download and attempt to download it.

new BrowserWindow({
  webPreferences: {
    plugins: false
  }
})

You may also want to checkout electron's DownloadItem api to do some manipulation on where the file will be saved.

2) Download the pdf through some other api

I'm not gonna give any specifics for this one because you should be able to find some information on this yourself, but basically if you want to download the file from somewhere, then you can use some other download API like an AJAX library to download the file and save it somewhere. This would potentially allow you to render the document in an electron window as well, since once you initiate the download you can probably redirect the window to the pdf url and have the native viewer handle it.

Long story short, it sounds to me like you don't really want to print from electron, you just want to save the pdf file that you're displaying. Printing from electron will render what you see on the screen, not the pdf document itself so I think you just misunderstood what the goal of print was. Hopefully this helps you, good luck!

=== EDIT ===

Unfortunately, I don't believe that there is a way to print the file directly from electron since electron printing is for printing the contents of electrons display. But you should be able to download the file via a simple request for the file (see above).

My recommendation for you would be to create a page for previewing the file. This would be an independent page, not the built in pdf viewer. You can then insert a button somewhere on the page to download the pdf via some means and skip any save location prompts (this should be easy enough to find documentation for).

Then, in order to have your preview, on the same page you can have a webview tag into your page, which will display the native pdf viewer. In order for the native pdf viewer to work in the webview tag, you must include the plugins attribute in the tag. It's a boolean tag, so it's mere presence is all that is needed such as <webview ... plugins> This turns on plugin support for that webview's renderer which is required for the pdf viewer.

You can modify the size styling of this tag on the page as you wish to suit your needs. A trick to get rid of the download and print options so that a user cannot press them is to append #toolbar=0 to the end of the pdf url to prevent the native pdf viewer from displaying the top toolbar with these buttons.

So, this way you can have your preview, ensure that the user can't use the built in download or print from the pdf viewer with the extra ui, and you can add another button to download it so it can be printed later.