Presenting PDF files by yourself

09/04/2010 § 43 Comments


When it comes to present a PDF file, the first solution that comes to mind is to use the UIWebView.

And as everybody already knows, there are many ways to get weird results when displaying PDF files using the UIWebView on iPhone SDK 3.2 both in the iPad Simulator and the actual device.

There are basically three ways to avoid these results:

  1. To present the PDF file inside a HTML content
  2. To reload the web view when the app change it’s orientation
  3. To write your own PDF presenter

The first approach does not present the PDF file nicely. It does not scale the content accordingly to the screen size or orientation so that it fits perfectly, and we have to agree that it doesn’t feel like a native app.

Using the second approach, you will be able to feel almost all the user experience that you want to, but rotating your app will cause a blank screen to appear (and lasts) until the document is fully loaded into memory again. If you intent to present small documents, this may be the better approach to take. And you could display a loading view or something like that to make it look better.

Following the third approach you will be able to do wherever you want to, but you will also need to bother with content displaying, resizing, scrolling, rotation, zooming and we can’t forget the look and feel (because you probably want to make it look like the native UIWebView, and this will require you to add shadows do each page…and we all know that the iPad device has serious problems with performance when it comes to shadows. But this is a topic for another day).

If after reading all that I told you, the chosen approach was the third one (or if you are a curious guy), I recommend you to keep reading ’til the end of this tiny post.

I do not plan to throw a lot of code at you (and the title of this post says by yourself), so I will not give you all the code you need to make a nice PDF presenter app. What I plan to do is just the startup, so that you can enjoy your code experience a little bit more.

Before we begin, it would be nice of you to review the Core Foundation Memory Management Guide if you are not familiar with it.

….

Let’s say that you have a document in your resources bundle.  A good beginning would be to open that document and display it nicely. Fortunately iOS provides us a library called Core Graphics that can help you on this not so hard task.

Actually Core Graphics enables us to reference a file as a PDF document, fetch it’s pages and draw them!

To make it simple, let’s define a method that returns a CGPDFDocumentRef from a file path that points to your document:

- (CGPDFDocumentRef)document {
   NSString *filePath = [[NSBundle mainBundle]
             pathForResource:@"presentation" ofType:@"pdf"];
   NSURL *url = [[NSURL alloc] initFileURLWithPath:filePath];
   CGPDFDocumentRef document = [self openDocument:(CFURLRef)url];
   [url release];
   return document;
}

As you may have noticed, we just used a type cast from NSURL to CFURLRef. This can be done because Core Foundation types are interchangeable in function or method calls with it’s bridged Foundation object.

Now let’s take a look on how we can handle the Core Foundation Url that we got to actually retrieve a CGPDFDocumentRef:

- (CGPDFDocumentRef)openDocument:(CFURLRef)url {
   CGPDFDocumentRef myDocument = CGPDFDocumentCreateWithURL(url);
   if (myDocument == NULL) {
      return 0;
   }
   if (CGPDFDocumentIsEncrypted (myDocument)
     || !CGPDFDocumentIsUnlocked (myDocument)
     || CGPDFDocumentGetNumberOfPages(myDocument) == 0) {
      CGPDFDocumentRelease(myDocument);
      return 0;
   }
   return myDocument;
}

Some of my fellows would say that the code above is ugly, and I kind of agree with them. But we have to keep in mind that we are not dealing with Objective-C at this point, so we might want to make it clear.

We are skipping some treatments that you probably want to do, like handling encrypted documents or locked ones. Also, if the document has no pages there is no reason to display it.

Now that we finally have a document reference to work with, I am pretty sure that the next step to take is to create the view that actually presents the PDF content.

What I suggest you to do, is to create a UIViewController and place these methods on it (don’t forget to use proper #pragma marks!) and add as a subview a class called something like PDFView that is responsible for displaying an entire PDF document. This PDFView class would implement the UIScrollViewDelegate protocol because you probably want to display more than one PDF page at once and having a UIScrollView as it child, you would get almost for free some behaviors like scrolling and zooming.

You could call a load method on this PDFView right after it is allocated that actually reads each PDF page and add it as a view to your UIScrollView.

- (void)load {
   NSInteger numberOfPages = CGPDFDocumentGetNumberOfPages(document);
   for (NSInteger pageIndex = 1; pageIndex <= numberOfPages;
        pageIndex++) {
      CGPDFPageRef page = CGPDFDocumentGetPage(document, pageIndex);
      PDFPageView *pageView = [[PDFPageView alloc] init];
      pageView.page = page;
      pageView.autoresizingMask =
              UIViewAutoresizingFlexibleHeight
            | UIViewAutoresizingFlexibleWidth;
      pageView.autoresizesSubviews = YES;
      pageView.backgroundColor = [UIColor whiteColor];
      [scrollView addSubview:pageView];
      [pageView release];
   }
}

Please note that page counting starts at 1 instead of 0. We are also setting some autoresizing flags, so that the SDK can handle that kind of stuff for us.

You probably will want to configure this flags on your UIScrollView, so that you can achieve a really nice autoresizing handling when rotating the app. For free.

The class that I called PDFPageView is the one that makes the magic. It represents a PDF page and is responsible for presenting it. This way, we can encapsulate page rendering from the rest of our features.

In order to render a CGPDFPageRef we need to override [UIView drawRect:]  and draw it ourselves:

- (void)drawRect:(CGRect)rect {
   CGContextRef context = UIGraphicsGetCurrentContext();
   CGContextSaveGState(context);
   CGFloat frameHeight = self.frame.size.height;
   CGContextTranslateCTM(context, 0, frameHeight);
   CGContextScaleCTM(context, 1.0, -1.0);
   CGContextDrawPDFPage(context, page);
   CGContextRestoreGState(context);
}

Every time that we want to draw something, we need a CGContextRef that represents our canvas. That is what we do on the first line.

It is a good practice to save the current drawing context state before modifying it, so that you can restore it to it’s original state right after you are done. That is because you may want to draw some other content or maybe some class that subclasses yours may want to do that.

Unfortunately, the Core Graphics PDF system uses the desktop coordinate system, that is flipped compared to the iPhone one. So we need to flip the coordinate system vertically. That is the reason we assign -1.0 to the factor by which to scale the y-axis of the coordinate space when calling CGContextScaleCTM.

For the same reason, we also need to displace the y-axis of the coordinate space by the amount of space needed to display the page. We do that by calling CGContextTranslateCTM. Finally we draw the page into the context using CGContextDrawPDFPage.

One thing that I suggest as an exercise (if you really want a good-looking app) is to use the PDFPageView frame and the actual PDF page  rect to resize the page so that it scales to fit without loosing it’s ratio. Keep in mind that you can do that easily within 20 lines of code.

Now it is you turn! Develop a nice app!

Tagged: , , , , ,

§ 43 Responses to Presenting PDF files by yourself

  • Mohammed O. Tillawy says:

    Thank you for your lovely contribution,
    But I had to make a tiny change to make “drawRect” called in the PDFPageView.

    In the function:
    – (void)load {
    I repaced:
    //PDFPageView *pageView = [[PDFPageView alloc] init];
    With:
    PDFPageView *pageView = [[PDFPageView alloc] initWithFrame:scrollView.bounds];

    • Good catch!

      Indeed I’ve made a more complete application to render PDF files and I did not add every method (like layoutSubviews), just the ones to make the rendering itself work. Seems I forgot this detail.

      Thank you very much!

  • Rohinton Collins says:

    Hi,
    How did you manage to redraw the PDF after a zoom. UIScrollView appears to modify the graphics state (CGContextScaleCTM) to achieve the zoom effect. But when it has finished I want to redraw the PDF at the current zoom to get rid of the pixellation. So I call [pdfPageView setNeedsDisplay] in the scrollViewDidEndZooming:withView:atScale: method. But back in my drawRect function, the CTM has been modified by the UIScrollView. So unmodified code in drawRect will result in the same scale/pixellation as we currently have! I need a way of restoring the graphics state, then to redraw the PDF, achieving the zoom with the CGPDFPageGetDrawingTransform function. Cheers for any help here. Much searching on the NET has currently not borne fruits. I don’t think that CFTiledLayer is the answer as this is about drawing smaller tiles of a much larger object, like a map. I just want to redraw the pdf page, but at a different zoom, as provided by the UIScrollView class in the scrollViewDidEndZooming:withView:atScale: delegate method, and with an offset.

    • I achieved this calculating the proper aspect ratio between the PDFPageView frame and the PDF page rect itself. Then I applied the scale to the actual PDFPageView frame, instead of the PDF page itself.

      The draw method basically need to be modified so that it looks like this:

      
      - (void)drawRect:(CGRect)rect {
      	CGContextRef context = UIGraphicsGetCurrentContext();
      	
      	CGFloat frameWidth = self.frame.size.width;
      	CGFloat frameHeight = self.frame.size.height;
      	
      	CGContextSaveGState(context);
      	CGContextTranslateCTM(context, 0, frameHeight);
      	CGContextScaleCTM(context, 1.0, -1.0);
      	
      	CGRect pageRect = CGPDFPageGetBoxRect(page, 
                         kCGPDFBleedBox);
      	CGFloat pageWidth = pageRect.size.width;
      	CGFloat pageHeight = pageRect.size.height;
      	CGFloat scaleRatio = 1;
      	
      	if (pageWidth < pageHeight) {
      		scaleRatio = frameHeight / pageHeight;
      	} else {
      		scaleRatio = frameWidth / pageWidth;
      	}
      		
      	CGFloat xOffset = (frameWidth - pageWidth 
                       * scaleRatio) / 2;
      	CGFloat yOffset = (frameHeight - pageHeight 
                       * scaleRatio) / 2;
      	
      	CGAffineTransform pdfTransform = 
                        CGPDFPageGetDrawingTransform(
                             page, kCGPDFMediaBox,
                             CGRectMake(xOffset, yOffset, pageWidth, 
                                   pageHeight), 0, true); 
      	CGContextConcatCTM(context, CGAffineTransformScale(
                      pdfTransform, scaleRatio, scaleRatio));
      	CGContextDrawPDFPage(context, page);
      	
      	CGContextRestoreGState(context);
      }
      
      
      

      To be fair, I have some code to properly calculate the actual view frame, but if you consider that the PDF page is scaled as the view (where it is presented) is scaled, then you should be good with that drawRect method.

      PS: Sorry for the code formatting.

      • Joe says:

        Firstly thank you for an excellent example of how to present PDFs in a UIScrollView, it’s certainly the most useful I’ve found in a few days research.
        I just wanted to ask a similar question as above: I’ve implemented the drawRect in my PDFPageView, however I just can’t seen to get the output to display without blurriness, and the positioning is an issue with it jumping after I zoom with an incomplete rect – could you expand slightly on how you update the PDFPageView from the scrollViewDidEndZooming:withView:atScale: delegate method?

      • hi, it was really useful but I’m having problems with the ratio. When i zoom mi app its scaling to a bigger size than it would. I thing its scaled twice. Here’s my code:

        – (void)drawRect:(CGRect)rect
        {

        CGFloat frameWidth = self.frame.size.width;
        CGFloat frameHeight = self.frame.size.height;

        CGContextRef context = UIGraphicsGetCurrentContext();

        CGRect pageRect = CGPDFPageGetBoxRect(page, kCGPDFBleedBox);

        pdfScale = self.frame.size.height/pageRect.size.height;
        CGFloat myscale = self.frame.size.height/pageRect.size.height;

        //[self drawInContext:UIGraphicsGetCurrentContext()];
        CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
        CGContextFillRect(context,pageRect);

        // We’re about to modify the context CTM to draw the PDF page where we want it, so save the graphics state in case we want to do more drawing
        CGContextSaveGState(context);
        // PDF page drawing expects a Lower-Left coordinate system, so we flip the coordinate system
        // before we start drawing.
        CGContextTranslateCTM(context, 0.0, self.frame.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        //CGContextScaleCTM(context, pdfScale,pdfScale);
        CGFloat pageWidth = pageRect.size.width;
        CGFloat pageHeight = pageRect.size.height;
        CGFloat scaleRatio = 1;

        if (pageWidth < pageHeight) {
        scaleRatio = frameHeight / pageHeight;
        } else {
        scaleRatio = frameWidth / pageWidth;
        }

        CGFloat xOffset = (frameWidth – pageWidth
        * scaleRatio) / 2;
        CGFloat yOffset = (frameHeight – pageHeight
        * scaleRatio) / 2;

        // CGPDFPageGetDrawingTransform provides an easy way to get the transform for a PDF page. It will scale down to fit, including any
        // base rotations necessary to display the PDF page correctly.
        // CGAffineTransform pdfTransform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, CGRectMake(0, 0, self.frame.size.width/myscale,self.frame.size.height/myscale), 0, true);
        // And apply the transform.
        //CGContextConcatCTM(context, pdfTransform);
        // Finally, we draw the page and restore the graphics state for further manipulations!
        CGAffineTransform pdfTransform =
        CGPDFPageGetDrawingTransform(page, kCGPDFMediaBox,CGRectMake(xOffset, yOffset, pageWidth,pageHeight), 0, true);
        CGContextConcatCTM(context, CGAffineTransformScale(pdfTransform, scaleRatio, scaleRatio));
        CGContextDrawPDFPage(context, page);
        CGContextRestoreGState(context);
        CGPDFPageRelease(page);
        CGPDFDocumentRelease(pdf);
        //CGContextRelease(context);
        }

        I'm using parts of some samples. That view its a subview of another view that is a subview of the scrollview. something like this on my code

        in a big horizontal paged uiscroll view i paint pages with

        PDFScrollView *svnuevo = [[PDFScrollView alloc] initWithFrame:CGRectMake(coordenadax, 0, 768, 1024) pagina:index];

        and PDFScrollView use a uiview:

        vistaCompleta = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 1024)];

        create a PDF page

        pdfVista = [[PDFView alloc] initWithFrame:CGRectMake(0, 0, 768, 1012) pagina:pg andscale:1];

        and finally PDFView have this

        – (id)initWithFrame:(CGRect)frame pagina:(int)pg andscale:(CGFloat)myScale
        {
        if ((self = [super initWithFrame:frame])) {

        // Set up the UIScrollView
        pdfScale = myScale;

        // Open the PDF document
        NSURL *pdfURL = [[NSBundle mainBundle] URLForResource:@"archive.pdf" withExtension:nil];
        pdf = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL);

        // Get the PDF Page that we will be drawing
        page = CGPDFDocumentGetPage(pdf, pg);

        CGPDFPageRetain(page);
        }
        return self;
        }

        the draw rect of PDFView its the first code where I'm implementing your ratio calculating. If you find my problem i ll be glad thanx!

  • New To iOS says:

    I am relatively new to iOS. drawRect is not getting called on the pageview. Would appreciate if you could post the code.

  • ali says:

    what about word doc files

  • New To iOS says:

    Thank you so much. That was it.

    However, only the first page in the pdf document is displayed and the scrolling is not working. Any pointers is greatly appreciated.

  • New To iOS says:

    Got it to work. the frame has to be set correctly in each call to initWithFrame for the PDFPageView.

  • New To iOS says:

    The links in the PDF doesn’t seem to be active. Does this require using PDFScanner?

  • New To iOS says:

    Maybe this issue is related to simulator. Just curious, what are the weird issues using UIWebView to display PDF content (other than an explicit setNeedsDisplay is required with rotation)?

    • Well, if you have a PDF file big enough..(let’s say 10Mb) once you rotate the device, it will take a second or two to correctly render the page you are looking at…in the meanwhile you will see a broken page (like if split in a half).

      That is really annoying.

  • Awesome approach to this. Thank you for sharing. This will scale up to all kinds of madness like zooming and background loading, too (I think).

  • shrabani says:

    i want to add an annotation in the pdf and also I want to make the pdf editable.is this possible to make select text and copy that selected text from the pds.If it is possible plz let me know how to do this.

  • Rocky says:

    First off.. thanks for the tutorial. I am a newbie to IOS. I have as far as I know followed your steps.
    – Created a UIViewController
    – Added a ScrollView to the controller.view.
    – Called load() to add PDFPageView to the ScrollView.

    However I am seeing only the last page and scroll is not working. I am not sure what I am missing. Any help is greatly appreciated.

    • Hey! Thanks for the feedback!

      Please check if:

      – you defined the pdf page view frame
      – you specified the correct coordinates for each pdf view frame origin
      – you defined the scrollView content size

      Please send me an email if you need any further help on this 😉

      • Rocky says:

        I have the following..

        – you defined the pdf page view frame
        the following is from the – (void) load {} function
        pageView = [[PDFPageView alloc] initWithFrame:theScrollView.bounds];

        – you specified the correct coordinates for each pdf view frame origin
        I am not sure about that.
        – you defined the scrollView content size

        I have this for content size .. dont think it is right though
        [theScrollView
        setContentSize:CGSizeMake(theScrollView.frame.size.width,
        theScrollView.frame.size.height + pageView.frame.size.height)]

  • Rocky says:

    A correct from my previous post. The content size that I used was
    theScrollView.contentSize = CGSizeMake(theScrollView.frame.size.width , theScrollView.frame.size.height* numberOfPages);

  • kano says:

    thanks, clear and very helpfull!
    it would be best if you put here source code.

  • quangas says:

    it would be nice to see the source code, its still not clear where everything goes when you’re a beginner.

    • Hi,

      if you take a moment to read the comments you will be able to get all the code you need to get this tutorial to work =)

      The whole idea is to present a way to render PDFs and not to give a PDF Viewer component =)

      But as soon as possible I will be adding a comment with the source code or something 😉

      Thanks!

  • Hagi says:

    Hi, thanks so much for this tutorial – it’s really useful. I would greatly appreciate seeing the exact source code files, as I’m having some trouble getting the PDF to re-render after zooming (more specifically I get it to re-render, but all sorts of weird positioning errors happen, and it seems the PDF image gets cut off along the edges? Using setNeedsDisplay).

    • Hi Hagi!

      I am sorry to tell you I don’t have the code anymore. A lot of people is asking for it :/

      Anyways, I would place each PDF page on it’s own UIScrollView and each UIScrollView/PDFPageView inside another UIScrollView. Then I would handle scrollEnabled accordingly. This way you can properly zoom a page without having “all sorts of weird positioning errors” 😉

      Also, always draw the page smaller than the view you are rendering into and centered. This will help you avoid cuts 🙂

      Cheers!

  • thuchu1 says:

    Hello,

    I’m super new to iOS coming from flash and needed a little help with this. Do these sections of code go on the ViewController.m file? And how can the scroll view be loaded under the [super viewDidLoad] section? This is the last piece of the puzzle I need to finish my app.

    • Hi!

      I am not sure if I got your question but here we go:

      1) Do these sections of code go on the ViewController.m file? What sections? To know if they are, cmd+click on the method. If it takes you to the UIViewController.h file, then it is.
      2) And how can the scroll view be loaded under the [super viewDidLoad] section? By adding its subviews. You should read the Apple docs on UIView lifecycle and this UIScrollView tutorial.

      Good luck!

  • Everything is ok with your code; however, I have a memory and performance issues.

    1) I have pdf that has 500 pages and its size is about 10 mb. When I load all the pages at once, the application crashes.

    2)Also, I tried to use small caching(5-10 pages); however, if I load a new pages to cache, then I have to wait about 10-20 seconds…

    What is your solution to these kind of problems…?

    • Hey!

      Nice question!

      Actually you can improve the code I provided in both performance and quality.

      You can achieve more quality while zooming for example, using a CATiledLayer instead of the usual CALayer.

      And you can improve performance and usability by loading one page at a time using the UIPageViewController provided by iOS 5.

      If you need to load more than one page at time for some reason (to provide a TOC with thumbnails for example..), you can make use of NSOperationQueue to handle the loading of each page and implement some sort of “view tiling” logic to render only those that are visible to the user (while scrolling for example).

      Cheers!

      • Thank you Cezar,

        I found a solution for my problem. It is like a NSOperationQueue logic. I’ll write this solution to my blog.

        Best Regards,

  • Kim says:

    Hello Cezar,

    Nice tutorial! I have a question though… I want to create a PDF annotator app (where I can draw on the PDF and stuff). I already made a UIView called drawing View where the user can draw.. my question is how can I load the drawing View and the PDF into one UIScrollView? I want each page of the PDF having its own drawing view and I am confused on how to do it.

    • Hi!

      I have made a very similar application and I did get good results.

      On my implementation, every pdf page is reprsented by a set of views. One of these views renders the pdf page itself, each one of the other views render a specific type of annotation (text, straight lines, free drawings..).

      All these views have the same size and position and they are all inside the same scrollview. I actually have one scrollview per page and each page is rendered by the page controller apple made available on iOS 5.

      The secret lies on storing any coordinate information in pageRect coordinates, so you can convert to the proper view cooridinate and propely handle orientation changes as well as zooming in or out.

      Good luck!

  • senthil says:

    i want to play a video from my pdf file reader(stand alone application) with a floating windows with options to make it full screen. how can i acheive it? am inneed your sugesstions. thankyou!

  • Qamar khan says:

    Hi All,
    I am fresher in ios development.Can someone help me out if there is code of this good walk through uploaded on Github or any other location for reference.

    Thanks
    Qamar

    • No, I didn’t add any example project on Github or anywhere else when I wrote this post.

      Will do when I have a chance. I am running with no time these days.

      The walkthrough provides enough information though.

  • Superb tutorial… working fine as expected.. But one thing i have one thing .. User couldn’t select the text on UIView… how to do that… Sorry for my english.. and thanks in advance

    • Thanks Rathna. The text is not selectable because we just implemented the rendering part. If you want the text to be selectable, you need to extend the post’s code to allow for that.

Leave a reply to Emrah Ayanoglu Cancel reply

What’s this?

You are currently reading Presenting PDF files by yourself at iOS Guy.

meta