Displaying PDF documents on iOS

iOS has a very nice library for the displaying of PDFs, but because of their large size, take up significant memory and often are unable to be displayed all at once.

In order to display large PDFs you must first know some UIView basics. The UIView display area is generally composed of any number of CALayers. There are several different types of CALayers which can be interchanged depending on what you want to display. For the display of heavy PDFs or very large images, we will use the CATiledLayer. This is a special layer which draws the layer content as a series of tiles, allowing the display of very large or memory intensive content such as huge images, PDFs, etc.

First we need to get a CGPDFPageRef from the PDF file. To do that we first get the PDF CGPDFDocumentRef, using the CGPDFDocumentCreateWithURL() function. From there we can retrieve the CGPDFPageRef by calling CGPDFDocumentGetPage(documentRef, pageNumber).

With the CGPDFPageRef in hand, we can now create our CATiledLayer. Below, is an example of the drawLayer method from a subclassed CATiledLayer:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
        CGPDFPageRef pageRef = myPageRef
        if (pageRef != nil && ctx != nil) {
            [(NSObject*)pageRef retain];
            //prevent releasing while drawing
            CGPDFPageRetain(pageRef);
            CGContextRetain(ctx);
            CGContextSaveGState(ctx);

            CGRect bounding = layer.bounds;            
            CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0);
            CGContextFillRect(ctx, CGContextGetClipBoundingBox(ctx));
            CGContextTranslateCTM(ctx, 0.0, bounding.size.height);
            CGContextScaleCTM(ctx, 1.0, -1.0); 
            CGContextConcatCTM(ctx, CGPDFPageGetDrawingTransform(pageRef, kCGPDFCropBox, bounding, 0, true));
            CGContextDrawPDFPage(ctx, pageRef);
            CGContextRestoreGState(ctx);
            CGContextRelease(ctx);
            CGPDFPageRelease(pageRef);
            [(NSObject*)pageRef release];
        }

}

First we retain both the pageRef and the CGContextRef. (It is not strictly necessary to retain the pageRef but due to the multithreaded nature of CATiledLayer rendering a little defensive programming doesn't hurt) and save the CGContextRef state.

Next, using the layer bounds the pdf background color (white) is filled in. Since the iPhone coordinate system is the reverse of that of the Mac, we must translate and scale the context to flip the coordinate system and force rendering from the bottom left corner. After concacting the context to the PDF rendering rect, we finally draw the PDF to the context.After drawing, restore and release the context state, and if your done with the CGPDFPageRef and CGPDFDocumentRef, release them with CGPDFPageRelease and CGPDFDocumentRelease respectively.

This will get you a basic tiled PDF display.

One unfortunate drawback is that display of a tiled layer is not exactly speedy, and the user will be left staring at a black page while the various tiles are being drawn. So how do we get something to be displayed immediately when the UIView is presented? The easiest way is to create another CALayer behind the CATiledLayer and use it to draw a low resolution image of the pdf content. Something like this in your UIView:

//set up the background image layer
self.imageLayer = [CALayer layer];
//set the size and contents with the image     
self.imageLayer.contents = (id) yourUIImage.CGImage;
//add the layers
[self.layer addSublayer:self.imageLayer];

But PDF display is still slow, memory intensive, etc and can create problems when paging quickly through a PDF document. Since the true value of displaying a PDF is for the display of clean text while zooming, one way to limit the damage is in the CATiledLayer to only draw the pdf when zoomed in above a threshold and use images for content at lower zoom scales. This will also limit exposure of your app to PDF rendering memory leaks that can be found in certain iOS versions. For example:

{
    if(self.zoomScale < 2.0 ){
        CGImageRef  cgImage = yourUIImage.CGImage;
        if( cgImage != nil )
        {        
            CGContextSaveGState( context );
            CGRect bounding = self.bounds;
            CGContextTranslateCTM(ctx, 0.0, bounding.size.height);
            CGContextScaleCTM(ctx, 1.0, -1.0); 
            CGContextDrawImage( context, bounding, cgImage );
            CGContextRestoreGState( context );
        }
    }else {
        CGPDFPageRef pageRef = myPageRef
        ...
    }
}

While this doesn't cover all of the details, it should be enough to get you started.