Display a PDF page on the iPhone and iPad
March 23rd, 2011
Displaying a PDF page in an iPhone/iPad application seems pretty simple since the iOS includes a PDF API but there a few pitfalls. The iOS SDK includes the ZoomingPDFViewer sample and there are also several examples on the internet that show how to do this. The thing with Appleโs ZoomingPDFViewer and the other examples is that they have several flaws: rotated pages are not handled correctly, the actual position of MediaBox and CropBox is not considered, they cannot scale higher than 100% and they do not clip the content relative to CropBox as any PDF viewer does. The test files available for download at the end of this article let you verify these issues.
This article will show how to take care of the above problems and display a PDF page at a specific position in the view with a user defined zoom level.
First we translate view coordinate system in the top left corner of the rectangle where the PDF page will be displayed:
1 | CGContextTranslateCTM(context, point.x, point.y); |
Then we set the zoom level. This is simply achieved by scaling the context with the zoom value divided by 100 (I assume the zoom parameter is given in percents: 100, 200, etc).
1 | CGContextScaleCTM(context, zoom / 100, zoom / 100); |
Next we set up the view coordinate system to match the page coordinate system. The images below show the relation between the view coordinate system and page coordinate system for each possible page rotation.
The mapping is achieved through several affine transformations, depending on the page rotation angle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | switch (rotate) { case 0: // Translate the origin of the coordinate system at the // bottom left corner of the page rectangle. CGContextTranslateCTM(context, 0, cropBox.size.height); // Reverse the Y axis to grow from bottom to top. CGContextScaleCTM(context, 1, -1); break; case 90: // Reverse the Y axis to grow from bottom to top. CGContextScaleCTM(context, 1, -1); // Rotate the coordinate system. CGContextRotateCTM(context, -M_PI / 2); break; case 180: case -180: // Reverse the Y axis to grow from bottom to top. CGContextScaleCTM(context, 1, -1); // Translate the origin of the coordinate system at the // top right corner of the page rectangle. CGContextTranslateCTM(context, cropBox.size.width, 0); // Rotate the coordinate system with 180 degrees. CGContextRotateCTM(context, M_PI); break; case 270: case -90: // Translate the origin of the coordinate system at the // bottom right corner of the page rectangle. CGContextTranslateCTM(context, cropBox.size.height, cropBox.size.width); // Rotate the coordinate system. CGContextRotateCTM(context, M_PI / 2); // Reverse the X axis. CGContextScaleCTM(context, -1, 1); break; } |
The size in points of the visible page area is given by the page CropBox property. The page CropBox width is always measured along the page X axis and the CropBox height is measured along the page Y axis (page coordinate system).
Because the page can have content outside the CropBox, everything outside the CropBox must be clipped.
1 2 3 | CGRect clipRect = CGRectMake(0, 0, cropBox.size.width, cropBox.size.height); CGContextAddRect(context, clipRect); CGContextClip(context); |
Before drawing the page on the view, we need to do one more thing. The bottom left corner of the page CropBox must be moved at the origin of the coordinate system.
1 | CGContextTranslateCTM(context, -cropBox.origin.x, -cropBox.origin.y); |
Now everything is in place, we can draw the page in the view.
1 | CGContextDrawPDFPage(context, page); |
The code above has been placed in the static method renderPage:inContext:atPoint:withZoom:
in the PDFPageRenderer
class (full source code available for download at the end of the article), the method is used like this:
1 2 3 4 5 6 | - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGPDFPageRef page = CGPDFDocumentGetPage(doc, 1); [PDFPageRenderer renderPage:page inContext:ctx atPoint:CGPointMake(10, 10) withZoom:50]; } |
A possible extension of the drawing method above is to render the PDF page in a given rectangle. The page should take all available space while keeping the aspect ratio.
We start by computing the size of the visible page area.
1 2 3 4 5 6 7 | CGRect cropBox = CGPDFPageGetBoxRect(page, kCGPDFCropBox); int pageRotation = CGPDFPageGetRotationAngle(page); CGSize pageVisibleSize = CGSizeMake(cropBox.size.width, cropBox.size.height); if ((pageRotation == 90) || (pageRotation == 270) ||(pageRotation == -90)) { pageVisibleSize = CGSizeMake(cropBox.size.height, cropBox.size.width); } |
The page must be scaled to fit the display rectangle. The scale factor is the minimum between scale factor on X axis and scale factor on Y axis.
1 2 3 | float scaleX = displayRectangle.size.width / pageVisualSize.width; float scaleY = displayRectangle.size.height / pageVisualSize.height; float scale = scaleX < scaleY ? scaleX : scaleY; |
Next we compute the aspect ratios for the display rectangle and visible page area. Based on these aspect ratios we compute the position of page top left corner relative to display rectangle top left corner.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Offset relative to top left corner of rectangle where the page will be displayed float offsetX = 0; float offsetY = 0; float rectangleAspectRatio = displayRectangle.size.width / displayRectangle.size.height; float pageAspectRatio = pageVisibleSize.width / pageVisibleSize.height; if (pageAspectRatio < rectangleAspectRatio) { // The page is narrower than the rectangle, we place it at center on the horizontal offsetX = (displayRectangle.size.width - pageVisibleSize.width * scale) / 2; } else { // The page is wider than the rectangle, we place it at center on the vertical offsetY = (displayRectangle.size.height - pageVisibleSize.height * scale) / 2; } CGPoint topLeftPage = CGPointMake(displayRectangle.origin.x + offsetX, displayRectangle.origin.y + offsetY); |
Having computed the page top left corner and scale factor, we're ready to display the page.
1 | [PDFPageRenderer renderPage:page inContext:context atPoint:topLeftPage withZoom:scale * 100]; |
The complete source code and test PDF files can be downloaded below.
The code above has been placed in the static method renderPage:inContext:inRectangle:
in the PDFPageRenderer
class, the method is used like this:
1 2 3 4 5 6 | - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGPDFPageRef page = CGPDFDocumentGetPage(doc, 1); [PDFPageRenderer renderPage:page inContext:ctx inRectangle:CGRectMake(10, 10, 200, 200)]; } |
Source code: PDFPageRenderer.zip
PDF files: TestFiles.zip
[relatedPosts]
March 30th, 2011 - 21:43
This is cool ;-).
April 19th, 2011 - 11:02
the code is not working with zoom.
how did you used it??
April 19th, 2011 - 13:48
Thanks for this code module ๐
It helped me alot to improve my memory management. ๐
But still wondering, why isn’t the zoom is working??
Can you please guide me how does it works with the zoom???
April 19th, 2011 - 19:31
The renderPage method has the zoom parameter which lets you scale the displayed page. I tried it with values from 25 to 300 and the displayed page was scaled correctly. Are you referring to this parameter or to some other zoom?
April 25th, 2011 - 14:57
Your aspect ratio test looks questionable…first “narrow” and “width” in the comments are synonyms, and should be expressed as “width” and height” or “narrow” and “tall”, second shouldn’t you be adjusting both offsetX and offsetY?
April 25th, 2011 - 21:04
I’ll work on my English in the future articles, thank you. Regarding the offsets, when you scale a page to fit inside a given rectangle, one edge of the page will always be placed over a rectangle edge, this is why only one offset is adjusted. For example the destination rectangle is 320×480, its aspect ratio is 320/480 = 0.667. Let’s say the page size is 612×792 (Letter), its aspect ratio is 0.772. The x scale factor is 320/612 = 0.523, the y scale factor is 480/792 = 0.606. In order to keep the page aspect ratio, the page must be scaled with 0.523 (minimum of scale x and scale y) and the scaled page size will be 320×414. It takes the entire rectangle width (offsetX is 0) and it is centered on vertical (offsetY is (480 – 414) / 2). If the page size is 612×1008 (Legal), its aspect ratio is 0.607. The x scale factor is 320/612 = 0.523, the y scale factor is 480/1008 = 0.476. In order to keep the page aspect ratio, the page must be scaled with 0.476 and the scaled page size will be 291×480. It takes the entire rectangle height (offsetY will 0) and it is centered on horizontal (offsetX is (320 – 291) / 2).
May 24th, 2011 - 20:12
If I am trying to create a button that when clicked will display all importable PDFs in a clickable list, how do I a) find all of the importable pdfs in the documents folder on the ipad and b) display those documents in a list that the user will be able to choose from?
May 26th, 2011 - 21:39
a) You can use the NSFileManager class and its methods to get the list of files in a specified folder. b) You can use a UITableView for displaying a list of items.
June 29th, 2011 - 15:13
I have an iPad app that displays pdf pages.I need to add annotations on the image (if exists on the pdf page) for which i need the coordinates at which the image is situated in the pdf page.I am able to get the image data from the XObject and the image width and height,but i also need the x and y coodrinate of the image.Any idea about how to obtain the coordinates of image by parsing pdf page?
June 30th, 2011 - 09:15
I’m preparing for July an article just on this matter, but I can send you just the code if you need it sooner.
July 11th, 2011 - 20:00
Do you have a target date for this posting?
July 11th, 2011 - 20:30
2nd half of July. The code is almost finished, I just have to write the article.
July 11th, 2011 - 21:00
Excellent!
July 13th, 2011 - 13:00
Hi,
Can you please send your personal mail id. Because i tried this code to draw 270 degree rotated pdf page. Your code does not show the page. It shows the white background screen. Also while try to draw rotated angle 0 degree page it’s working fine. Can you please help me?
July 14th, 2011 - 11:26
The code had a bug for 270 degrees rotation but I fixed it, both on the website and in the zip archive. The email address is available in the About page.
July 19th, 2011 - 20:49
This code did wonders, thank you. I am also interested in your update about the parsing of files.
Do you have any ideas on
1) paging (i.e. display more than one pdf page, like a book or the sort, rotating each page as necessary)?
2) reading encrypted files (partitioned or not)
If you have any new info, please email me (artur.sampaio@live.com). Once again thank you =)
July 20th, 2011 - 19:58
1) You could use the renderPage:inContext:inRectangle method to render multiple pages side by side, split the view in 2 and render different pages in each half. There is the leaves framework for page turning effect, I have not tried yet to integrate it with my code.
2) Quartz provides support for reading encrypted files, after you get the CGPDFDocumentRef object you can unlock it with the CGPDFDocumentUnlockWithPassword method. This method needs to be called right after obtaining the CGPDFDocumentRef object. I’m not sure what you mean by partitioned PDF files.
September 9th, 2011 - 09:00
Hi,
I am try to load big pdf file of just two page but file size is 27.7MB so application just crashes due to memory issue. I am drawing pdf page in Layer(CATiledLayer) of UIVIew. Loading only one page at a time. This page is completely loading in iPhone4.0 but not working in iPad. Can you point me in right direction?
September 14th, 2011 - 18:26
Please take a look at this open source PDF viewer for iOS: http://www.vfr.org/2011/09/pdf-reader-viewer-v2-2/
It handles large PDF files without problems, it might help you.
September 19th, 2011 - 13:34
Hi ,
Thank you for this post it was very halpfull.
where can I find the update from july?
Regarding entering annotations?
thank you
September 27th, 2011 - 22:04
I’m not sure what update are you referring to.
October 3rd, 2011 - 15:06
Hi. How I can reorganize code to support rotation to different interface orientations?
Thank you.
February 28th, 2012 - 12:00
HI. Great Tutorial. Can you please explain if there is a method to save a single pdf page from a pdf document.
Thanks
March 2nd, 2012 - 20:11
As far as I know the CGPDF API does not support this functionality.
March 2nd, 2012 - 14:54
Cool !
I add a function as follow, you might wants to add it in.
+ (CGSize) pageSize: (CGPDFPageRef) page {
CGRect cropBox = CGPDFPageGetBoxRect(page, kCGPDFCropBox);
int rotate = CGPDFPageGetRotationAngle(page);
switch (rotate) {
case 90:
case 270:
case -90:
return CGSizeMake(cropBox.size.height, cropBox.size.width);
break;
}
// for 0, 180, -180
return cropBox.size;
}
March 2nd, 2012 - 20:13
Thank you for the code.
March 9th, 2012 - 11:38
first thanks for the code. i was interested in the rotation part. sadly that didnt work for me.
i have some pdfs to show. some pdfs have larger width than height. then it gets rotated automatically. i have tried your rotation solution (switch case) but no luck. do you have test working of that? thanks in advance.
March 9th, 2012 - 12:41
never mind. i got it to render properly using your code. thanks!!!
October 17th, 2012 - 15:01
Thanks for such a great work. Simple and clear PDF Rendering code it is.
I have one problem while making pdf page as double spread ie., as in normal books, which shows me a separator or a white line between those two pages.
Kindly resolve this.
October 22nd, 2012 - 18:40
You have a method that renders a PDF page in a given rectangle. Split your view in 2 rectangles and draw one page in each rectangle. In this way you have double spread.
December 17th, 2012 - 13:41
Thanks for the code.
I would like to apply dynamic zoom on PDF page according to the resolution of PDF files so user can read fine text. I am working on magazine app so I don’t know what kind of resolution of PDF files. How can i calculate zoom scale.
Please provide me solution.
Regards,
Utsav
December 17th, 2012 - 17:27
PDF files contain vector graphics, they do not have a resolution. You should do a few tests with your magazine to find the best scale factor and use it.
December 19th, 2012 - 12:57
Thank you very much for the reply.