Friday, January 20, 2006

Silent print a PDF file in Acrobat Reader (for PDF files served on the Internet)

Update: Please do not forget to check the other PDF and print related links in the sidebar


Some of you might have been awaiting this.
You maybe disappointed with the results of this article though, so dont expect too much - I'll explain what is possible with existing software and code ( but not voodoo programming : that's not the kind of code that's bug free ).

Prerequisites
  • Acrobat Javascript Guide (pdf file) and Reference (pdf file),
  • Some basic knowledge of Java servlets,
  • Some knowledge of the iText PDF library or atleast some enthusiasm to learn it.
Précis

I will demonstrate how to generate a PDF document with the iText PDF library. The document will have embedded Acrobat Javascript in it. Using the Acrobat Javascript commands I will ensure that the PDF document will be printed (to the default printer) when it is opened, followed by an attempt to close the document (which may not succeed when the document is opened inside a browser using the Acrobat Reader plugin or BHO). The PDF with the embedded Acrobat Javascript is served by a Java servlet which allows for the solution to be demonstrated over the internet.

Source Code

The WAR (Web ARchive) file containing the servlet and the iText library can be downloaded here(You can mail me if the download fails). You could straightaway deploy it on Apache Tomcat or any other servlet container or even a J2EE application server.

What's going on?

The important stuff is being done here:

/*
The output stream of the servlet to which the PDF document is sent.
*/
ServletOutputStream out = response.getOutputStream();

/*
Create an iText document - that's a PDF document that we're creating.
*/
Document document = new Document();

/*
Create a ByteArrayOutputStream to which the document will be written (the document will NOT be created on as a disk file at the server - use FileOutputStream for that.
*/
ByteArrayOutputStream baos = new ByteArrayOutputStream();

try {

/*
We get an instance of Pdfwriter. The instance is "listening" to the document object, and is "tied" to the byte array output stream. This means - whenever we add elements to the document the writer object picks it up and transfers it to the output stream.
*/
PdfWriter writer = PdfWriter.getInstance(document, baos);

/*
I'm setting the viewer preferences so that the user is not able to see the menubar or scrollbar inside the browser when this file is opened using the Acrobat Reader PDF plugin. This could be treated as a "security measure" for preventing users from saving the file or printing the file once again. Be careful - it's a viewer preference and not a security provision. It can be reverted by the user because it's only an indication to the Reader plugin on how the document is to be presented when it is initially loaded.
*/
writer.setViewerPreferences( PdfWriter.HideMenubar | PdfWriter.HideToolbar | PdfWriter.HideWindowUI );

/*
We have to open the document for writing information after the header and meta-information is added. Which means that we have to "prepare" the document before we are have to write the user-visible information.
*/
document.open();

/*
We now add a document level Javascript action so that the entire action is executed when the document is opened. To see what other options are available, you will have to go through the Acrobat Javascript Reference and Guide (links are same as above).
The necessary information can be found in the Doc object provided by the Acrobat Javascript model. The Doc object can be referenced usually by using the "this" object.
*/
writer.addJavaScript(
"this.print({bUI: false,bSilent: false,bShrinkToFit: true});" +
"\r\n" +
"this.closeDoc();"
);

/*
We add some dummy statement to be printed.
I hate to print a blank document, but I dont waste paper.
So, if you are environment conscious, please enter whitespaces instead.
Do not (God forbid), remove the line of code below to save on paper. LOL.
*/
document.add(new Chunk("Silent Auto Print"));

/*
You have to close the document when you're done with it.
*/
document.close();
}
catch (DocumentException e)
{
e.printStackTrace();
}

/*
I'm setting the content type of the response so that the browser will recognize that it is going to receive a PDF file. However, do not be too confident about this.
Some browsers - especially IE and Opera are known to do "content-sniffing" to determine what is to be done with a server's response. And that could change the equation drastically.
*/
response.setContentType("application/pdf");

/*
Some browsers are known to flip and throw up when they dont know how much data is going to be received by them. So it is wise to set the content length header before sending data to the browser.
*/
response.setContentLength(baos.size());

/*
We wrote the document to a ByteArrayOutputStream. Now flush that stream to the servlet's response object.
*/
baos.writeTo(out);

/*
Flush the servlet's response object so that the servlet responds to the browser's request.
*/
out.flush();


A Trial Run

You could go to this demonstration page and see how it works. Be sure to try this out in different environments - with and without Acrobat Reader browser plugin installed, with and without SP2 installed on Windows XP, different browsers (especially Firefox and Opera), and even when the user is able to save the file and then open it. You'll learn quite a lot on why I chose to write the servlet this way and not any other way. Consistency matters a lot when it comes to the internet.


15 comments:

pascal said...

hey thanks for the article.
i might try it out. too bad we still need to open Acrobat for printing. we have these pro users and they might print several 100 docs a day. in the end we might stick with the applet solution (we are more or less in control of the browser... so applet would work)

have a nice day

Vineet Reynolds said...

Thanks for the feedback.
In my next article I'm going to explore MeadCo's plugin so that you could use that instead (if the budget permits).
It seems to be a robust plugin and doesnt have the startup load time associated with a Java applet.

pascal said...

yeah, startup time of applets is crap. but we could fly around this with some hidden frame solution. to many boundaries in this project :-)

Jose said...

Hi. Very nice example.

It could be useful to catch any possible exception in the JavaScript code. Something like that:

String js = "try { this.print({bUI: false, bSilent: true}); this.closeDoc(true); } catch(e) {/*handle exception*/}";
writer.addJavaScript(js);

When a document (PDF) is shown inside the browser, trying to "close" that document will raise an exception.

Vineet Reynolds said...

Yes, that's right Jose.
I chose to omit that just in case the programmers using iText wouldn't understand that.

The ability to close the PDF document from the browser triggers an exception as you rightly pointed out. However that is the pitfall of using the Adobe Acrobat plugin to view pdf files in the browser. A plugin written differently might show different behavior. It's very difficult to get consistent results across various browsers which is what I chose to show here.

Thanks for your time and inputs.

Anonymous said...

Hey,
Can anybody tell how can I print mutiple PDF using silent print.

Vineet Reynolds said...

Print multiple files -> by downloading each file using individual HTTP GET/POST requests and then printing them as shown previously. You can use Javascript to issue multiple HTTP requests to download the collection of files in a loop.

Anonymous said...

Hi, i've been tryin desperatly to print on a network printer and it won't work for some odd reason. The name I use is the same one I get when I list them through script in my pdf (i.e format is \\PcName\PrinterName). So my question is, is it possible to print through a network printer (except by doing it through the default printer since that's too limited)?

Thanks in advance for the answer and for all that useful information.

Bob

Vineet Reynolds said...

Bob,
I cant really figure out whether this is a problem with a specific document, or a specific client on the network, a printer issue, or a problem with the printer driver.
In case, here's the checklist published by Adobe.
In case you have trouble accessing the printer, try changing the UNC name of the printer or ensure that your script is not mangling the UNC name.
Lastly, network printers in a (mostly) Windows environment, might required additional setup to ensure that specific users have the necessary privileges to access the printer as 'Printer Operators'. Microsoft has outlined that here.

Anonymous said...

hi, i try to do same thing in asp.net but, when Adobe try to open the file, an exception is occured. It is the file contains errors so adobe cannot open it. Can anybody help me?

Vineet Reynolds said...

Well, if the file contains errors, you might want to try and find out if its a valid PDF document.

sandeep said...

sandeep said,

this is okay, but when i am downloading the war file it is giving errors, as u suggested, that if problem, occurs, u will mail, so i am writing this.

Vineet Reynolds said...

Link updated

Anonymous said...

Hi Vineet,

The link for war file download is invalid again. Could you provide a new link? Thanks,

Osmund

Vineet Reynolds said...

Hi Osmund,
Thanks for notifying me. I've updated the link.