← Back to the blog

PrintQueue - PDF printing with Acrobat Reader end to end solution

Last week I posted twice on printing PDF documents. This is the last post of this unplanned series. Today I would like to present an end to end solution including ColdFusion PrintQueue event gateway with two C# applications for controlling Acrobat Reader. Quick summary of the problem we faced last week: ColdFusion cfprint tag has problems when printing documents created with LiveCycle Designer. Not all items from documents, specially repeated regions are printed. At Monochrome we had to figure out how to print them. Simplest solutions are best and we decided to go with Acrobat Reader batch printing capabilities.

Previous articles can be found here:


  1. LiveCycle Designer + cfpdfform + cfpdf + cfprint = fail

  2. rinting PDF files created with LiveCycle designer

Acrobat Reader solution seems to work really well, there is only one very specific problem. It appears there is about 70 users printing documents very often. Unfortunately Acrobat Reader is not closing documents after sending them to the printer. We had to add component which will shutdown Acrobat Reader once it does so. This is causing next problem: how to make sure Acrobat Reader is always opened? First implementation of our solution used queue which allowed printing just one document at a time. That was because of how our component for killing Acrobat Reader was designed. It was looking for AcroRd32 process running on the server and when found it was killing it. When multiple AcroRd32 processes were running they were killed as well. Because of that there was no reason to run multiple Acrobat Reader instances in different threads. Friday last week we developed second PrintQueue version. This time it is an event gateway with two separate C# components. This time it uses process ID instead of process name.

Let’s take a look at first C# app – AcrobatReaderStartPid.exe:

[sourcecode language="csharp"]
using System;
using System.Diagnostics;
namespace AcrobatReaderStartPid {
 class Program {
  static void Main(string[] args) {
   ProcessStartInfo psi = new ProcessStartInfo();
   psi.Arguments = "/n /t \"" + args[1] + "\" \"" + args[2] + "\"";
   psi.FileName = args[0];
   Process p = Process.Start(psi);
   Console.WriteLine(p.Id);
  }
 }
}
[/sourcecode]

It is very simple code. It takes 3 arguments in following order:

  • Full path to Acrobat Reader executable
  • Full path to PDF file that will be sent to the printer
  • Printer name, may be network name

The purpose of the code is to start Acrobat Reader process, read process ID and write it to the console. Event gateway reads it and passes it to second C# component – AcrobatReaderCtlKiller.

[sourcecode language="csharp"]
using System;
using System.Diagnostics;

namespace AcrobatReaderCtlKiller {
 class Program {
  static void Main(string[] args) {
   try {
    Process proc = Process.GetProcessById(Int32.Parse(args[0]));
    proc.Kill();
   }
   catch (Exception) { }
  }
 }
}
[/sourcecode]

This executable takes just one argument – process ID. Then it finds Acrobat Reader by process ID and closes it, simple as that. Using PID we could start multiple instances of Acrobat Reader without worrying of killing incorrect one.

Most important part of this solution is event gateway itself. First of all print job – it is represented by PrintJob Java class.

[sourcecode language="java"]
public class PrintJob {
 private String file;
 private String printer;
 private String owner;
 public PrintJob(String file, String printer, String owner) {
  super();
  this.file = file;
  this.printer = printer;
  this.owner = owner;
 }
 public String getFile() {
  return file;
 }
 public void setFile(String file) {
  this.file = file;
 }
 public String getPrinter() {
  return printer;
 }
 public void setPrinter(String printer) {
  this.printer = printer;
 }
 public String getOwner() {
  return owner;
 }
 public void setOwner(String owner) {
  this.owner = owner;
 }
}
[/sourcecode]

And now PrintQueue class – this is ColdFusion event gateway main class. Parts of the code were taken from sample EmptyGateway class coming with ColdFusion installation.

[sourcecode language="java"]
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import coldfusion.eventgateway.CFEvent;
import coldfusion.eventgateway.Gateway;
import coldfusion.eventgateway.GatewayServices;
import coldfusion.eventgateway.Logger;

public class PrintQueue implements Gateway {
 // The handle to the CF gateway service
 protected GatewayServices gatewayService = null;
 // ID provided by EventService
 protected String gatewayID = "";
 // Listener CFC paths for our events
 protected String[] listeners = null;
 // Path to my configuration file
 protected String config = null;
 // The thread that is running the listener
 protected Thread listenerThread = null;
 // Should we shutdown?
 protected boolean shutdown = false;
 // timeout for waiting for listener thread to die - 10 seconds
 protected static final int TEN_SECONDS = 10 * 1000;
 // Out status
 protected int status = STOPPED;
 // logger
 protected Logger log;
 // event gateway properties
 protected Properties properties = new Properties();
 // acrobat reader executable path:
 protected String acrobatReaderPath = "";
 // AcrobatReaderStartPid exe path:
 protected String acrobatStarterPath = "";
 // AcrobatReaderCtlKiller exe path:
 protected String acrobatReaderKillerPath = "";
 // queue holding print jobs when all current print threads are busy:
 protected ArrayList queue;
 // currently printing queue:
 protected ArrayList currentlyInPrint;
 // max simultaneous print jobs:
 protected int maxPrintJobs = 10;
 // when true debug output is added to print-queue log:
 protected boolean debug = true;
}
[/sourcecode]

Next step is the class constructor with the loadProperties() method. Here they are:

[sourcecode language="java"]
 public PrintQueue(String gatewayID, String config) {
  this.gatewayID = gatewayID;
  this.config = config;
  this.gatewayService = GatewayServices.getGatewayServices();
  this.queue = new ArrayList();
  this.currentlyInPrint = new ArrayList();
  this.log = gatewayService.getLogger("print-queue");
  try {
   FileInputStream propsFile = new FileInputStream(config);
   properties.load(propsFile);
   propsFile.close();
   this.loadProperties();
  }
  catch (IOException e) {
   log.warn("PrintQueue(" + gatewayID + ") Unable to read configuration file '" + config + "'.", e);
  }
 }
 private void loadProperties() {
  this.acrobatReaderKillerPath = properties.getProperty("acrobatReaderKillerPath");
  this.acrobatStarterPath = properties.getProperty("acrobatStarterPath");
  this.acrobatReaderPath = properties.getProperty("acrobatReaderPath");
  this.maxPrintJobs = Integer.parseInt(properties.getProperty("maxPrintJobs"));
  this.debug = (Integer.parseInt(properties.getProperty("debug")) == 1);
  if (this.debug) {
   log.info("PrintQueue(" + gatewayID + ") Loaded with following configuration:");
   log.info("PrintQueue(" + gatewayID + ") ------------------------------------");
   log.info("PrintQueue(" + gatewayID + ") Acrobat Reader Path: " + this.acrobatReaderPath);
   log.info("PrintQueue(" + gatewayID + ") Acrobat Reader Starter: " + this.acrobatStarterPath);
   log.info("PrintQueue(" + gatewayID + ") Acrobat Reader killer: " + this.acrobatReaderKillerPath);
   log.info("PrintQueue(" + gatewayID + ") ------------------------------------");
  }
 }
[/sourcecode]

Up to now the code is quite simple. At the beginning we have class definition with all properties used inside and all required imports. PrintQueue constructor is a standard event gateway constructor. It takes gateway instance name declared in CF Administrator and path to .properties file. Properties are loaded by loadProperties() method. Here is a sample .properties file that we’re using:

acrobatReaderKillerPath=c:/Inetpub/AcrobatReaderCtlKiller.exe
acrobatStarterPath=c:/Inetpub/AcrobatReaderCtlStarter.exe
acrobatReaderPath=C:/Program Files/Adobe/Reader 9.0/Reader/AcroRd32.exe
# This is in fact number of Acrobat Reader instances running at the same time:
maxPrintJobs=5
debug=0

To fulfil interface requirements bunch of standard event gateway methods are required. They don’t require special comments:

[sourcecode language="java"]
 public void setCFCListeners(String[] listeners) {
  this.listeners = listeners;
 }
 public coldfusion.eventgateway.GatewayHelper getHelper() {
  // We have no helper class to provide to the CFML programmer
  return null;
 }
 public void setGatewayID(String id) {
  gatewayID = id;
 }
 public String getGatewayID() {
  return gatewayID;
 }
 public void start() {
  status = STARTING;
  // Start up listener thread
  Runnable r = new Runnable() {
   public void run() {
    listener();
   }
  };
  listenerThread = new Thread(r);
  shutdown = false;
  listenerThread.start();
  status = RUNNING;
 }
 public void stop() {
  status = STOPPING;
  // tell generator to stop
  shutdown = true;
  try {
   listenerThread.interrupt();
   listenerThread.join(TEN_SECONDS);
  }
  catch (InterruptedException e) {
   // ignore
  }
  status = STOPPED;
 }
 public void restart() {
  stop();
  start();
 }
 public int getStatus() {
  return status;
 }
[/sourcecode]

It is time to handle incoming messages. The outgoingMessage message is called when sendGatewayMessage method is executed from CFC component (BTW: if anyone knows why method for handling incoming messages is called outgoingMessage I would appreciate short explanation). This method expects 3 arguments sent from the CFC:

  • full path to PDF file
  • printer name
  • owner name, string representing ID of the user who created print job or full name

When executed it first checks how many instances of Acrobat Reader are running at the moment. If maxPrintJobs is not reached print job is sent directly to Acrobat Reader, otherwise print job is queued for later print. Here is the code:

[sourcecode language="java"]
 public String outgoingMessage(coldfusion.eventgateway.CFEvent cfmsg) {
  Map data = cfmsg.getData();
  // we have two params here:
  String file = data.get("FILE").toString();
  String printer = data.get("PRINTER").toString();
  String owner = data.get("OWNER").toString();
  if (this.debug)
   log.info("PrintQueue(" + gatewayID + ") Print job received: " + printer + " <- " + file);
  synchronized (this.currentlyInPrint) {
   // if we have more than N print jobs running
   // we need to save print job for later:
   if ( this.currentlyInPrint.size() >= this.maxPrintJobs ) {
    synchronized (this.queue) {
     if (this.debug)
      log.info("PrintQueue(" + gatewayID + ") Sending print job to queue.");
     PrintJob job = new PrintJob(file, printer, owner);
     this.sendCFCMessage(job, "queuing", "add");
     this.queue.add( job );
    }
   } else {
    if (this.debug)
     log.info("PrintQueue(" + gatewayID + ") Sending print job directly to Acrobat Reader.");
    this.executePrint(new PrintJob(file,printer,owner));
   }
  }
  return "OK";
 }
[/sourcecode]

Next discussed method is listener() method. This one is executed as Runnable from start() method. It is the thread observing the print queue. It decides when to send the print job to Acrobat Reader if there are any jobs waiting in print queue.

[sourcecode language="java"]
 protected void listener() {
  while (!shutdown) {
   // check if there are jobs in queue:
   PrintJob job = null;
   synchronized (this.queue) {
    if ( this.queue.size() > 0 ) {
     synchronized (this.currentlyInPrint) {
      if ( this.currentlyInPrint.size() < this.maxPrintJobs ) {
       if (this.debug)
        log.info("PrintQueue(" + gatewayID + ") Queued print job found. Moving to print ("+this.currentlyInPrint.size()+").");
       job = (PrintJob)this.queue.get(0);
       this.queue.remove(job);
      }
     }
    }
   }
   if ( job != null ) {
    this.executePrint(job);
   }
   // sleep here?
   try { Thread.sleep(100); } catch (InterruptedException ex) { /* ignore */ }
  }
 }
[/sourcecode]

Two last PrintQueue methods are executePrint() and sendCFCMessage(). Here they are:

[sourcecode language="java"]
 private void executePrint(PrintJob job) {
  PrintWorker worker = new PrintWorker(job);
  worker.start();
 }
 private void sendCFCMessage(PrintJob job, String status, String action) {
  CFEvent event = new CFEvent(gatewayID);
  event.setCfcMethod("onPrintStatus");
  Hashtable mydata = new Hashtable();
  mydata.put("FILE", job.getFile().substring(job.getFile().lastIndexOf('/')+1));
  mydata.put("PRINTER", job.getPrinter());
  mydata.put("STATUS", status);
  mydata.put("ACTION", action);
  mydata.put("OWNER", job.getOwner());
  event.setData(mydata);
  event.setGatewayType("PrintQueue");
  event.setOriginatorID("");
  for (int i=0; i<listeners.length; i++) {
   // Set CFC path
   event.setCfcPath(listeners[i]);
   // send it to the event service
   gatewayService.addEvent(event);
  }
 }
[/sourcecode]

The sendCFCMessage() method sends messages back to CFC listeners. This allowed us to provide real-time print queue feedback for Flex application users. File name does not tell them probably what exactly is going on but because we are passing their names back and forth they at least have some idea where their job is in the queue.

Last part of the event gateway is internal class called PrintWorker. It extends java.lang.Thread and is used to execute Acrobat Reader instance and send document to the printer.

[sourcecode language="java"]
 class PrintWorker extends Thread {
  private PrintJob job;
  public PrintWorker(PrintJob job) {
   this.job = job;
  }
  public void run() {
   synchronized (currentlyInPrint) {
    if (debug)
     log.info("PrintQueue(" + gatewayID + ") Adding job to CURRENTLY PRINTING jobs queue.");
    currentlyInPrint.add(job);
   }
   sendCFCMessage(job, "sending to printer", "void");
   try {
    // execute Acrobat Reader command:
    Process p = Runtime.getRuntime().exec(
     acrobatStarterPath+" \"" + acrobatReaderPath + "\" \"" + job.getFile() + "\" \"" + job.getPrinter() + "\"");
    BufferedReader rd = new BufferedReader(new InputStreamReader(p.getInputStream()));
    // just one line - process id:
    String pid = rd.readLine().trim();
    if (debug)
     log.info("PrintQueue(" + gatewayID + ") Acrobat Reader started with process id " + pid + ". Sleeping...");
    // sleep - let AR send document to the printer:
    try { Thread.sleep(5000); } catch (InterruptedException ex) { /* ignore */ }
    // execute Acrobat Killer
    if (debug)
     log.info("PrintQueue(" + gatewayID + ") Document sent to the printer. Killing Acrobat Reader " + pid + ". Cleaning.");
    Runtime.getRuntime().exec(acrobatReaderKillerPath + " " + pid);
    // cleaning up:
    File pdfFile = new File(job.getFile());
    pdfFile.delete();
   }
   catch (IOException ex) {
    if (debug)
     log.warn("PrintQueue(" + gatewayID + ") Can't print job.", ex);
   }
   synchronized (currentlyInPrint) {
    if (debug)
     log.info("PrintQueue(" + gatewayID + ") Removing job from CURRENTLY PRINTING jobs queue.");
    currentlyInPrint.remove(job);
   }
   sendCFCMessage(job, "printed", "remove");
  }
 }
[/sourcecode]

And that is all the code for the PrintQueue event gateway. To install event gateway successfully a CFC file is required. PrintQueue.cfc is the file which handles all messages coming from event gateway. It is also used as RemoteObject by Flex. As you can see in Java source code CFC messages are sent in three different situations:

  • when print job is queued (outgoingMessage)
  • when print job is sent to the printer, Acrobat Reader starting (listener)
  • when print job was sent by Acrobat Reader to the printer (possibly printer is starting printing at this point)

Below is the CFC that handles printer queue at ColdFusion side:

[sourcecode language="cf"]
<cfcomponent>
 
 <cffunction name="onPrintStatus" output="no">
  <cfargument name="CFEvent" type="struct" required="yes" />
  <!--- Get the message --->
  <cfset var data=cfevent.DATA />
  <!---
   There are two properties:
    - data.FILE
    - data.PRINTER
    - data.STATUS
    - data.ACTION
  --->
  <cfswitch expression="#data.action#">
   <cfcase value="add">
    <cfif not structKeyExists(application, "printQueue")>
     <cfset application.printQueue = arrayNew(1) />
    </cfif>
    <cfset arrayAppend(application.printQueue, data) />
   </cfcase>
   <cfcase value="remove">
    <cfscript>
     tArr = arrayNew(1);
     for (i=1; i lte arrayLen(application.printQueue); i=i+1)
      if ( application.printQueue[i].file neq data.file )
       arrayAppend(tArr, application.printQueue[i]);
     application.printQueue = tArr;
    </cfscript>
   </cfcase>
   <cfdefaultcase>
    <cfscript>
     for (i=1; i lte arrayLen(application.printQueue); i=i+1)
      if ( application.printQueue[i].file neq data.file ) {
       application.printQueue[i].status = data.status;
       break;
      }
    </cfscript>
   </cfdefaultcase>
  </cfswitch>
 </cffunction>
 
 <cffunction name="getPrintQueue" access="remote" output="false" returntype="array">
  <cfif not structKeyExists(application, "printQueue")>
   <cfset application.printQueue = arrayNew(1) />
  </cfif>
  <cfreturn application.printQueue />
 </cffunction>
 
</cfcomponent>
[/sourcecode]

The onPrintStatus() method manages printQueue stored in application scope. Second method is used by Flex to pull print queue every N millisecond. Lastly let’s see how messages are sent to event gateway:

[sourcecode language="cf"]
<cffunction name="print" output="false" returntype="void" access="remote">
 <cfargument name="file" type="string" />
 <cfargument name="printerName" type="string" />
 <cfargument name="owner" type="string" />
 <cfscript>
  props = structNew();
  props.method="outgoingMessage";
  props.FILE = arguments.file;
  props.PRINTER = arguments.printerName;
  props.OWNER = arguments.owner;
  status = SendGatewayMessage("PrintQueue-#CGI.SERVER_NAME#", props);
 </cfscript>
</cffunction>
[/sourcecode]

And that is all.

Full source code of C# applications, event gateway and PrintQueue.cfc can be downloaded from here.

All source code is available under MIT license.

Running the code

Event gateways can be executed using ColdFusion Enterprise only. Event gateway was created with Java 1.4.2 so it should run fine with ColdFusion 7. To run the code export event gateway as JAR file and drop it to {CFUSION_INSTALL_DIR}/lib directory and restart ColdFusion. Next go to ColdFusion Administrator, Event Gateways section. Create new gateway type:

  • Name: PrintQueue
  • Description: whatever description you want
  • Java class: uk.co.monochrome.print.queue.PrintQueue

Next go to Gateway Instances page and create new gateway instance:

  • Gateway ID: PrintQueue-YOUR.HOST
  • Gateway type: select newly created type
  • CFC path: type full PrintQueue.cfc path
  • Configuration file: type full .properties file path

Don’t forget to deploy exe files and change paths in .properties file. Once it is done you can start your gateway.

Posted by Neil Middleton on 10 Nov 2008

blog comments powered by Disqus

From our portfolio

Jo's Trust
Website
JoTrust-Thumbnail
Charis Grants
Application
charis_thumbnail
iMoneymanager
Application
imoneymanager_small
www.flickr.com

Archives

From our blog

We’re looking for Ruby on Rails (RoR) developers to join our team!

Posted by Niklas Richardson on 12 Apr 2012

Monochrome are looking for a couple of enthusiastic Ruby on Rails developers to join the team – Both Lead and Junior/graduate roles. You don’t need to have years of Ruby on Rails...

The need for applications just keeps on growing

Posted by Adrian Munn on 24 May 2011

After 12 years in the industry experience has always told us to adapt and modify strategies in your business in order to survive and thrive. Monochrome are not adverse to taking on new challenges...

Places we would like to go...

Posted by Neil Middleton on 21 Jan 2011

Quite often in the office we will end up having a conversation about some particular company, that does something on a massive scale, and has a relevance to either technology or some sort of large...