Friday, January 8, 2010

High resolution PDFs from Flex

I was recently working on a mechanism to save some panels (similar to some Dashboard graphics from the Klok 2 dashboard view) created in a Flex application to the user's harddrive by converting them to PDF. The obvious choice for PDF creation is AlivePDF. However, the quality of the resulting PDF was...well terrible. The problem was that instead of trying to recreate every last bit of my app in the PDF, I was just passing a reference to each panel into the pdf.addImage method like so:

pdf.addImage(myPanel)


What this actually does is take a low resolution screenshot (72 DPI) of the component and add it as an image to the pdf. So when you print the pdf, the quality is pretty bad. After some digging I found this post which seemed to be exactly what I was looking for. I thought that simply doing this would do the trick:

var image:ImageSnapshot = ImageSnapshot.captureImage(myPanel, 300, new PNGEncoder());

The problem is caused by the fact that Alive PDF doesn't support transparent PNGs which is what is returned when using the PNGEncoder class which is part of Flex.

A little more digging (several hours worth actually) turned up this post which again sounded like my solution. Applying the code fix from that got me almost there. I created my own NonTransparentPNGEncoder which was basically a copy of the built in PNGEncoder with this new version of internalEncode() method:
private function internalEncode(source:Object, width:int, height:int,
transparent:Boolean = false):ByteArray
{
// The source is either a BitmapData or a ByteArray.
var sourceBitmapData:BitmapData = source as BitmapData;
var sourceByteArray:ByteArray = source as ByteArray;
if (sourceByteArray)
sourceByteArray.position = 0;
// Create output byte array
var png:ByteArray = new ByteArray();
// Write PNG signature
png.writeUnsignedInt(0x89504E47);
png.writeUnsignedInt(0x0D0A1A0A);
// Build IHDR chunk
var IHDR:ByteArray = new ByteArray();
IHDR.writeInt(width);
IHDR.writeInt(height);
IHDR.writeByte(8); // bit depth per channel
IHDR.writeByte(2); // color type: RGBA
IHDR.writeByte(0); // compression method
IHDR.writeByte(0); // filter method
IHDR.writeByte(0); // interlace method
writeChunk(png, 0x49484452, IHDR);
// Build IDAT chunk
var IDAT:ByteArray = new ByteArray();
for (var y:int = 0; y <>
{
IDAT.writeByte(0); // no filter
var x:int;
var pixel:uint;
for (var j:int = 0; j <>
{
pixel = sourceBitmapData.getPixel(j, y);
IDAT.writeByte(pixel >> 16 & 0xFF);
IDAT.writeByte(pixel >> 8 & 0xFF);
IDAT.writeByte(pixel & 0xFF);
}
}
IDAT.compress();
writeChunk(png, 0x49444154, IDAT);
// Build IEND chunk
writeChunk(png, 0x49454E44, null);
// return PNG
png.position = 0;
return png;
}

One final change and I finally had it working:

pdf.addPage();
var image:ImageSnapshot = ImageSnapshot.captureImage(page, 300, new NonTransparentPNGEncoder());
var resize:Resize = new Resize ( Mode.FIT_TO_PAGE, Position.CENTERED );
pdf.addImageStream(image.data, ColorSpace.DEVICE_RGB, resize);



Then I just save my pdf to a ByteArray and use the FileReference to save it.

var bytes:ByteArray = pdf.save(Method.LOCAL);
var file:FileReference = new FileReference();
file.save(bytes, filename);

Keep in mind that although the panels are high resolution, they are still bitmap graphics. So, you will not be able to copy and paste any of your text out of the resulting PDF.

In order for all this to work, I am using 0.1.5 Beta version of AlivePDF, Flex 3.4 and Flash Player 10.

UPDATE: One last note. Because this is generating a 300DPI version of your component, it takes some time. A few seconds for a few page document.

7 comments:

Jamie McDaniel said...

Thanks, this post was really helpful! I copied the mx.graphics.codec.PNGEncoder and replaced the internalEncode function with the one you gave. Except that your for-loops look like they were parsed incorrectly by the blog. Here is what I used:

for (var y:int = 0; y < height; y++)
{
IDAT.writeByte(0); // no filter
var x:int;
var pixel:uint;
for (x = 0; x < width; x++)
{
pixel = sourceBitmapData.getPixel(x, y);
IDAT.writeByte(pixel >> 16 & 0xFF);
IDAT.writeByte(pixel >> 8 & 0xFF);
IDAT.writeByte(pixel & 0xFF);
}
}

Quasimondo said...

You might also want to check out Eugene's Async PNG encoder which is able to export even huge bitmaps: http://blog.inspirit.ru/?p=378

And for exporting full featured PDFs you should have a look at the brand new PurePDF by Alessandro Crugnola: http://www.sephiroth.it/weblog/archives/2010/02/purepdf_a_complete_actionscript_pdf_l.php

Ɓukasz said...

Why not resize the display object, addImage, resize back?

Scott said...

can someone help me out with this?

i edited the PNGEncoder class as specified (instead of creating my own custom class)

and used the following code:

claimPDF.addPage();
var image:ImageSnapshot = ImageSnapshot.captureImage(combination_chart, 300, new PNGEncoder());
claimPDF.addImageStream(image.data);

var bytes:ByteArray = claimPDF.save(Method.LOCAL);
var f:FileReference = new FileReference();
f.save(bytes,"report.pdf");


the above gives me the following error:

Implicit coercion of a value of type mx.graphics.codec:PNGEncoder to an unrelated type mx.graphics.codec:IImageEncoder

thanks!

Rob McKeown said...

@Scott - What version of the Flex SDK are you using?

DG said...

Thank You very much for this! Its a brilliant thing that you combined all these code and came out with this gem of a solution! And thanks to Jamie too for putting in the correction. Fantastic!!!

Mediamonkey said...

I just use this:

// set up a matrix for capturing the target at the correct dpi
if (dpi <= 0) dpi = Capabilities.screenDPI;
var matrix:Matrix = new Matrix(dpi/Capabilities.screenDPI, 0, 0, dpi/Capabilities.screenDPI);
var data:BitmapData = ImageSnapshot.captureBitmapData(target, matrix);

// add target image in cm @72 dpi, but fill image @300 dpi
pdf.addImage(new Bitmap(data), new Resize(Mode.NONE, Position.LEFT), x, y, cmTargetWidth, cmTargetHeight, 0, 1.0, true, ImageFormat.PNG);