One-click Silent printing from HTML page

Subinoy RoySubinoy Roy
7 min read

Document printing is a ubiquitous requirement in manufacturing environments, with users needing to print labels and PDFs at various stages of their workflows.

Our specific use case involved downloading PDF documents from pre-signed URLs and printing them using either the system's default printer or a designated printer without prompting the user to select a printer (silent printing).

This could be achieved in many ways.

  • Browser Plugin - Users can install a browser plugin to initiate printing. But this would require us to create separate plugins and additionally, every user would have to install the plugin onto their preferred browser. The users don't have that permission.

  • Kiosk Mode - Kiosk mode can print a web page with a simple window.print() function in JavaScript. Kiosk mode is not applicable in our case as we have a general-purpose web application that is not running in a very controlled environment where a kiosk runs.

  • Local Print Server - We could set up a local print server that can be accessed through an API from the user's browser. When a link is clicked, the server receives a request to send the document directly to the printer. However, setting up a local print server involves additional technical and administrative challenges.

  • Automated Silent Printing Using a Desktop Application - We can create a browser extension or web application that intercepts print requests. By extracting the necessary information from the request, the application can silently download the document and start the printing process, bypassing the standard print dialog.

The fourth solution, Automated Silent Printing Using a Desktop Application appears to be the most practical option for our needs. It offers centralized control over the entire printing process, ensuring compatibility across various browsers without requiring a local print server.

We created an Electron application using JavaScript. Why an Electron app and not anything else? That's because we have a team consisting of mostly Java/Spring developers for the back end and some JavaScript/React developers for the front end. I had earlier worked on a POC to interface with handheld scanners for an earlier project which was built using ElectronJS. Considering the team composition and prior experience, we went on to create an Electron application.

This is how the Printing application would work:

  • Initiate the printing application with a request payload containing the information required for printing

  • The printing application extracts information from the payload

  • The document is downloaded

  • A print job is initiated by the printing application

Prerequisite: Create a basic Electron App. Following the basic getting started tutorial from the electronjs official website.

1. Invoking the printing application - On our web page, we can have a custom URL that could have a certain protocol. For example, we have a URL https://google.com. Here, https is the protocol, and it's handled by browsers. We can create a custom protocol that can be handled by our custom application.

Let's give our protocol a name: ghostprint

When the app's 'ready' event is fired, we can add the following code to set the application as the default handler for our protocol:

app.setAsDefaultProtocolClient("ghostprint");
pp.whenReady().then(() => {
    app.setAsDefaultProtocolClient(PROTOCOL); // Registers this app as handler of the protocol "noprmptprnt"
    // More code ...
}

Read more about setAsDefaultProtocolClient() function here

Let's write a simple JavaScript code to call our application using a custom protocol. This code is run when a button with the id PrintBtn is clicked on a web page:

$('#PrintBtn').on('click', function(event){
    event.preventDefault();
    const PROTOCOL_PFX = "ghostprint://payload=";
    const payLoad = {
        "url": "https://pdfobject.com/pdf/sample.pdf",
        "requestType": "get",
        "payloadBody":{"data":"dummy"},
        "printerName":"Microsoft Print to PDF"
    }
    payloadEnc = encodeURIComponent(JSON.stringify(payLoad));
    window.location.href = PROTOCOL_PFX+payloadEnc; // Calls our printing app
});

You can extract information from the payLoad which will contain information about the Print Instructions.

2. Get the list of printers attached with the system - Once you extract the printing instructions, you have every information needed for performing the printing. But before you start printing, you have to get the list of printers attached to the system.

win.webContents.getPrintersAsync().then(printers => {
    // More Code ...
    // 
    if(payloadRefined.printerName){  
        printers.forEach(printer => {  
            if(payloadRefined.printerName===printer.name){  
                passedPrinterExists = true;  
            }
        })
    }
    // More Code ...
}

3. Download file - If a printer is found, you can start downloading the file. The file URL can be found in the Payload. We are using Axios for downloading the file.

async function downloadFile(url, requestType, payloadBody) {  
  const axiosConfig = {  
    url: url,  
    responseType: 'stream',  
    data: payloadBody  
  };  

  // More Code...

  return new Promise((resolve, reject) => {  
    axios(axiosConfig)  
      .then((response) => {
        // generating file name
        const pdfFilePath = path.join(app.getPath('downloads'), v4() + '.pdf');  
        const writer = fs.createWriteStream(pdfFilePath);  
        response.data.pipe(writer);  

        writer.on('finish', () => {  
          resolve(pdfFilePath);
        });  
        // More Code...  
      });  
  });  
}

4. Printing - Printing can be achieved by simply calling an embedded executable file. You can find many solutions which uses SumatraPDF which supports an extensive set of Command Line Arguments that are very well documented here. There is an NPM library pdf-to-print you can use for this purpose. You can have a look at a very good article Printing PDF in Node.js with Electron to see how it works.

But we wanted to be in total control of the printing process. We are primarily a Java development team and have enough experienced Java Developers. So, we ended up writing a small Java Program using Apache PDFBox library. We also had to embed JRE inside the application distribution to ensure that the JRE is not required to be installed separately.

The java Program

import org.apache.pdfbox.pdmodel.PDDocument;  
import org.apache.pdfbox.printing.PDFPageable;  
import org.apache.pdfbox.Loader;  

import javax.print.PrintService;  
import javax.print.PrintServiceLookup;  
import java.awt.print.PrinterJob;  
import java.io.File;  
import java.io.IOException;  

public class PDFPrinter {  
    public static void main(String[] args) {  

        // More code  ...

        for(int i = 0; i<args.length; i=i+2){  
            String flag = args[i];  
            String value = args[i+1];  
            // Validate the parameters...
        }  
        printPDF(pdfPath, printerName);  
    }  
    public static void printPDF(String pdfPath, String printerName) {  

        // More Code...

        // Find the specified print service  
        PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);  
        PrintService printService = null;  
        if(printerName!=null && !printerName.isBlank()) {  
            for (PrintService service : printServices) {  
                if (service.getName().equalsIgnoreCase(printerName.replace("\"",""))) {  
                    printService = service;  
                    break;  
                }  
            }  
            if (printService == null) {  
                System.err.println("Printer not found: " + printerName);  
                System.exit(1);  
            } else {  
                job.setPrintService(printService);  
            }  
        } else{  
            // Find the default print service  
            printService = PrintServiceLookup.lookupDefaultPrintService();  
            if (printService != null) {  
                job.setPrintService(printService);  
            }  
        }  

        // Set the document to be printed  
        job.setPageable(new PDFPageable(document));  

        // Print the document  
        job.print();  

        // More Code...

    }  
}

Generate an executable jar with all the required dependencies.

Embed JRE and the JAR file - In the package.json file, add the following code in build section

"build": {

    "extraResources": [  
    {  
        "from": "local-resources/print/",  
        "to": "electron-resources/print"  
    }

]

Inside the project directory, create the following structure and place the jar and JRE

.
└───ghostprint
    └───print
        ├───app-lib
        |   └───<executable-jar-with-dependencies>
        └───win-jre
            └───<place JRE content here>

Calling the JAR with parameters - Use execFile() function from child_process to run the executable JAR file

async function executePrint(print_command) {  
  const { printerName, pdfPath } = print_command;  
  try{  
    // More Code ...
    const args = [];  
    args.push('-jar');  
    args.push(jarPath);  
    args.push('-path');  
    args.push(`${pdfPath}`);  
    if (printerName) { // If not passed, it will be default printer  
      args.push('-printer');  
      args.push(`\"${printerName}\"`);  
    }  

    execFile(winJrePath, args, function (err, data) {  
      if (err) {  
        showDialog('Print Error', `There was an error while printing the PDF ${pdfPath}.`);  
      }  
      app.quit();  
    });  
  } catch(err) {  
    showDialog('Error',err);  
    app.quit();  
  }  
}

You can check out the repo code (You must add JRE content). You can run

  1. npm install

  2. npm run dist

The Windows installer file ghostprint Setup 0.0.1.exe will be generated inside the dist directory. Once you install this in Windows, you can invoke printing from any button by calling the following JavaScript function.

$('#PrintBtn').on('click', function(event){
    event.preventDefault();
    const PROTOCOL_PFX = "ghostprint://payload=";
    const payLoad = {
        "url": "https://pdfobject.com/pdf/sample.pdf",
        "requestType": "get",
        "payloadBody":{"data":"dummy"},
        "printerName":"Microsoft Print to PDF"
    }
    payloadEnc = encodeURIComponent(JSON.stringify(payLoad));
    window.location.href = PROTOCOL_PFX+payloadEnc; // Calls our printing app
});

Disclaimer

The code provided here is a sample code, enough to give you an idea about what can be done. This code is not production ready and there is no optimization. The code that was finally deployed in production has a lot of added functionalities and security features. But this will give you a base to start with.

Repo:
The Electron App code: https://github.com/subinoy-roy/ghostprint
The Java Program: https://github.com/subinoy-roy/ghostprintcompanion

1
Subscribe to my newsletter

Read articles from Subinoy Roy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Subinoy Roy
Subinoy Roy

Technology enthusiast, extremely opinionated, eager to play with new technologies. I am a photographer too and not bad at it.