Links navigation in a PDF document on iPhone and iPad
June 21st, 2011
iOS provides good support for displaying PDF files, but it lacks any support for the interactive features in the PDF files, such as annotations, links, form fields. In this article I'll show how to implement navigation in a PDF document using the links defined on document's pages. The project attached to this article is based on Apple's ZoomingPDFViewer sample that has been updated to support links navigation.
Links in a PDF file are defined using link annotations. Like any other annotations they are listed in the page's Annots entry. They are identified as links by the "Link" value stored in the Subtype entry in the annotation dictionary. Links can perform a variety of actions when they are clicked: they can display another page in the document or a page from another document, display a webpage, launch an external executable, submit form data to a server or execute custom JavaScript code. None of these actions are actually implemented in iOS, they need to be implemented by the developer based on the information available in the PDF file. In this article I'll focus on internal document links that display another page in the same document and web links.
The sample project is based on Apple's ZoomingPDFViewer sample. The TiledPDFView
class is the view class that displays the PDF page. It also stores the list of links on the page in an array. When the page is loaded in the view the list of links is populated. The loadPageLinks:
method is responsible for filling the links list. It loops through the Annots array defined in page's dictionary and checks if the annotation Subtype entry is "Link". If so a PDFLinkAnnotation
object is created and added to the list.
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 | - (void)loadPageLinks:(CGPDFPageRef)page { CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(page); CGPDFArrayRef annotsArray = NULL; // PDF links are link annotations stored in the page's Annots array. CGPDFDictionaryGetArray(pageDictionary, "Annots", &annotsArray); if (annotsArray != NULL) { int annotsCount = CGPDFArrayGetCount(annotsArray); for (int j = 0; j < annotsCount; j++) { CGPDFDictionaryRef annotationDictionary = NULL; if (CGPDFArrayGetDictionary(annotsArray, j, &annotationDictionary)) { const char *annotationType; CGPDFDictionaryGetName(annotationDictionary, "Subtype", &annotationType); // Link annotations are identified by Link name stored in Subtype key in annotation dictionary. if (strcmp(annotationType, "Link") == 0) { PDFLinkAnnotation *linkAnnotation = [[PDFLinkAnnotation alloc] initWithPDFDictionary: annotationDictionary]; [pageLinks addObject: linkAnnotation]; [linkAnnotation release]; } } } } } |
- (void)loadPageLinks:(CGPDFPageRef)page { CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(page); CGPDFArrayRef annotsArray = NULL; // PDF links are link annotations stored in the page's Annots array. CGPDFDictionaryGetArray(pageDictionary, "Annots", &annotsArray); if (annotsArray != NULL) { int annotsCount = CGPDFArrayGetCount(annotsArray); for (int j = 0; j < annotsCount; j++) { CGPDFDictionaryRef annotationDictionary = NULL; if (CGPDFArrayGetDictionary(annotsArray, j, &annotationDictionary)) { const char *annotationType; CGPDFDictionaryGetName(annotationDictionary, "Subtype", &annotationType); // Link annotations are identified by Link name stored in Subtype key in annotation dictionary. if (strcmp(annotationType, "Link") == 0) { PDFLinkAnnotation *linkAnnotation = [[PDFLinkAnnotation alloc] initWithPDFDictionary: annotationDictionary]; [pageLinks addObject: linkAnnotation]; [linkAnnotation release]; } } } } }
The PDFLinkAnnotation
class defines a link on the page. It stores the annotation dictionary for retrieving later the target of the link. The annotation rectangle (the position of the link on the page) is transformed into a CGRect
object for faster hit-testing. The class has methods for testing if the user touched the annotation and for retrieving the link's target.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #import <Foundation/Foundation.h> @interface PDFLinkAnnotation : NSObject { CGPDFDictionaryRef annotationDictionary; CGRect rect; } - (id)initWithPDFDictionary:(CGPDFDictionaryRef)newAnnotationDictionary; - (BOOL)hitTest:(CGPoint)point; - (NSObject*)getLinkTarget:(CGPDFDocumentRef)document; - (CGPDFArrayRef)findDestinationByName:(const char *)destinationName inDestsTree:(CGPDFDictionaryRef)node; @end |
#import <Foundation/Foundation.h> @interface PDFLinkAnnotation : NSObject { CGPDFDictionaryRef annotationDictionary; CGRect rect; } - (id)initWithPDFDictionary:(CGPDFDictionaryRef)newAnnotationDictionary; - (BOOL)hitTest:(CGPoint)point; - (NSObject*)getLinkTarget:(CGPDFDocumentRef)document; - (CGPDFArrayRef)findDestinationByName:(const char *)destinationName inDestsTree:(CGPDFDictionaryRef)node; @end
Highlight the links on the PDF page
The links available on a PDF page are usually not highlighted in any way. The text under link annotation may give you a hint that it behaves like a link. In a desktop PDF viewer when moving the mouse cursor over the link, it usually changes into a hand, but on iPhone and iPad there is no mouse so there is no visual feedback related to page links for the end-user. So after the page is displayed on the screen it would be nice to highlight the links in any way.
For rendering the page on the screen I used the PDFPageRenderer
class developed in this article: Display a PDF page on the iPhone and iPad.
After the page has been displayed the links are highlighted using yellow rectangles. The convertPDFPointToViewPoint:
method is used to convert the annotation rectangle from PDF coordinate system to view coordinate system. The relation between the view coordinate system and PDF page coordinate system is described in the article mentioned above.
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 | - (CGPoint)convertPDFPointToViewPoint:(CGPoint)pdfPoint { CGPoint viewPoint = CGPointMake(0, 0); CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox); int rotation = CGPDFPageGetRotationAngle(pdfPage); switch (rotation) { case 90: case -270: viewPoint.x = pageRenderRect.size.width * (pdfPoint.y - cropBox.origin.y) / cropBox.size.height; viewPoint.y = pageRenderRect.size.height * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width; break; case 180: case -180: viewPoint.x = pageRenderRect.size.width * (cropBox.size.width - (pdfPoint.x - cropBox.origin.x)) / cropBox.size.width; viewPoint.y = pageRenderRect.size.height * (pdfPoint.y - cropBox.origin.y) / cropBox.size.height; break; case -90: case 270: viewPoint.x = pageRenderRect.size.width * (cropBox.size.height - (pdfPoint.y - cropBox.origin.y)) / cropBox.size.height; viewPoint.y = pageRenderRect.size.height * (cropBox.size.width - (pdfPoint.x - cropBox.origin.x)) / cropBox.size.width; break; case 0: default: viewPoint.x = pageRenderRect.size.width * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width; viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - (pdfPoint.y - cropBox.origin.y)) / cropBox.size.height; break; } viewPoint.x = viewPoint.x + pageRenderRect.origin.x; viewPoint.y = viewPoint.y + pageRenderRect.origin.y; return viewPoint; } |
- (CGPoint)convertPDFPointToViewPoint:(CGPoint)pdfPoint { CGPoint viewPoint = CGPointMake(0, 0); CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox); int rotation = CGPDFPageGetRotationAngle(pdfPage); switch (rotation) { case 90: case -270: viewPoint.x = pageRenderRect.size.width * (pdfPoint.y - cropBox.origin.y) / cropBox.size.height; viewPoint.y = pageRenderRect.size.height * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width; break; case 180: case -180: viewPoint.x = pageRenderRect.size.width * (cropBox.size.width - (pdfPoint.x - cropBox.origin.x)) / cropBox.size.width; viewPoint.y = pageRenderRect.size.height * (pdfPoint.y - cropBox.origin.y) / cropBox.size.height; break; case -90: case 270: viewPoint.x = pageRenderRect.size.width * (cropBox.size.height - (pdfPoint.y - cropBox.origin.y)) / cropBox.size.height; viewPoint.y = pageRenderRect.size.height * (cropBox.size.width - (pdfPoint.x - cropBox.origin.x)) / cropBox.size.width; break; case 0: default: viewPoint.x = pageRenderRect.size.width * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width; viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - (pdfPoint.y - cropBox.origin.y)) / cropBox.size.height; break; } viewPoint.x = viewPoint.x + pageRenderRect.origin.x; viewPoint.y = viewPoint.y + pageRenderRect.origin.y; return viewPoint; }
The opposite corners of the annotation's rectangle are converted to view points and these points are used to draw the yellow rectangle over the page.
1 2 3 4 5 6 7 8 9 10 11 12 13 | for (int i = 0; i < [pageLinks count]; i++) { PDFLinkAnnotation *linkAnnotation = [pageLinks objectAtIndex: i]; CGPoint pt1 = [self convertPDFPointToViewPoint: linkAnnotation.pdfRectangle.origin]; CGPoint pt2 = CGPointMake( linkAnnotation.pdfRectangle.origin.x + linkAnnotation.pdfRectangle.size.width, linkAnnotation.pdfRectangle.origin.y + linkAnnotation.pdfRectangle.size.height); pt2 = [self convertPDFPointToViewPoint: pt2]; CGRect linkRectangle = CGRectMake(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y); CGContextAddRect(context, linkRectangle); CGContextStrokePath(context); } |
for (int i = 0; i < [pageLinks count]; i++) { PDFLinkAnnotation *linkAnnotation = [pageLinks objectAtIndex: i]; CGPoint pt1 = [self convertPDFPointToViewPoint: linkAnnotation.pdfRectangle.origin]; CGPoint pt2 = CGPointMake( linkAnnotation.pdfRectangle.origin.x + linkAnnotation.pdfRectangle.size.width, linkAnnotation.pdfRectangle.origin.y + linkAnnotation.pdfRectangle.size.height); pt2 = [self convertPDFPointToViewPoint: pt2]; CGRect linkRectangle = CGRectMake(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y); CGContextAddRect(context, linkRectangle); CGContextStrokePath(context); }
Links activation on the PDF page
The page being displayed on the screen, the user can touch it to activate a link. When a touch is received, the touch point is transformed from view coordinates to PDF coordinates. The convertViewPointToPDFPoint:
method in TiledPDFView
class takes care of the conversion.
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 | - (CGPoint)convertViewPointToPDFPoint:(CGPoint)viewPoint { CGPoint pdfPoint = CGPointMake(0, 0); CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox); int rotation = CGPDFPageGetRotationAngle(pdfPage); switch (rotation) { case 90: case -270: pdfPoint.x = cropBox.size.width * (viewPoint.y - pageRenderRect.origin.y) / pageRenderRect.size.height; pdfPoint.y = cropBox.size.height * (viewPoint.x - pageRenderRect.origin.x) / pageRenderRect.size.width; break; case 180: case -180: pdfPoint.x = cropBox.size.width * (pageRenderRect.size.width - (viewPoint.x - pageRenderRect.origin.x)) / pageRenderRect.size.width; pdfPoint.y = cropBox.size.height * (viewPoint.y - pageRenderRect.origin.y) / pageRenderRect.size.height; break; case -90: case 270: pdfPoint.x = cropBox.size.width * (pageRenderRect.size.height - (viewPoint.y - pageRenderRect.origin.y)) / pageRenderRect.size.height; pdfPoint.y = cropBox.size.height * (pageRenderRect.size.width - (viewPoint.x - pageRenderRect.origin.x)) / pageRenderRect.size.width; break; case 0: default: pdfPoint.x = cropBox.size.width * (viewPoint.x - pageRenderRect.origin.x) / pageRenderRect.size.width; pdfPoint.y = cropBox.size.height * (pageRenderRect.size.height - (viewPoint.y - pageRenderRect.origin.y)) / pageRenderRect.size.height; break; } pdfPoint.x = pdfPoint.x + cropBox.origin.x; pdfPoint.y = pdfPoint.y+ cropBox.origin.y; return pdfPoint; } |
- (CGPoint)convertViewPointToPDFPoint:(CGPoint)viewPoint { CGPoint pdfPoint = CGPointMake(0, 0); CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox); int rotation = CGPDFPageGetRotationAngle(pdfPage); switch (rotation) { case 90: case -270: pdfPoint.x = cropBox.size.width * (viewPoint.y - pageRenderRect.origin.y) / pageRenderRect.size.height; pdfPoint.y = cropBox.size.height * (viewPoint.x - pageRenderRect.origin.x) / pageRenderRect.size.width; break; case 180: case -180: pdfPoint.x = cropBox.size.width * (pageRenderRect.size.width - (viewPoint.x - pageRenderRect.origin.x)) / pageRenderRect.size.width; pdfPoint.y = cropBox.size.height * (viewPoint.y - pageRenderRect.origin.y) / pageRenderRect.size.height; break; case -90: case 270: pdfPoint.x = cropBox.size.width * (pageRenderRect.size.height - (viewPoint.y - pageRenderRect.origin.y)) / pageRenderRect.size.height; pdfPoint.y = cropBox.size.height * (pageRenderRect.size.width - (viewPoint.x - pageRenderRect.origin.x)) / pageRenderRect.size.width; break; case 0: default: pdfPoint.x = cropBox.size.width * (viewPoint.x - pageRenderRect.origin.x) / pageRenderRect.size.width; pdfPoint.y = cropBox.size.height * (pageRenderRect.size.height - (viewPoint.y - pageRenderRect.origin.y)) / pageRenderRect.size.height; break; } pdfPoint.x = pdfPoint.x + cropBox.origin.x; pdfPoint.y = pdfPoint.y+ cropBox.origin.y; return pdfPoint; }
Having converted the touch point to a PDF point, we test all the links on the page to see if there is one located at that point. The links can overlap, the z-order is given by the links position in the Annots array (and correspondingly the pageLinks array), top most link is located at the end of the array. Because of this the link hit testing is performed from the end of the array to the beginning. If a link of found, its target is loaded, whether it is another page or a URL.
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 | // Test if there is a link annotation at the point. // The z-order for the links is defined by the link position in the pageLinks array. // The last link in the array is the top most. for (int i = [pageLinks count] - 1; i >= 0; i--) { PDFLinkAnnotation *link = [pageLinks objectAtIndex: i]; if ([link hitTest: pdfPosition]) { CGPDFDocumentRef document = CGPDFPageGetDocument(pdfPage); NSObject *linkTarget = [link getLinkTarget: document]; if (linkTarget != nil) { if ([linkTarget isKindOfClass:[NSNumber class]]) { NSNumber *targetPageNumber = (NSNumber *)linkTarget; CGPDFPageRef targetPage = CGPDFDocumentGetPage(document, [targetPageNumber intValue]); if (targetPage != NULL) { [self setPage: targetPage]; [self setNeedsDisplay]; } } else { if ([linkTarget isKindOfClass: [NSString class]]) { NSString *linkUri = (NSString *)linkTarget; NSURL *url = [NSURL URLWithString: linkUri]; [[UIApplication sharedApplication] openURL: url]; } } } break; } } |
// Test if there is a link annotation at the point. // The z-order for the links is defined by the link position in the pageLinks array. // The last link in the array is the top most. for (int i = [pageLinks count] - 1; i >= 0; i--) { PDFLinkAnnotation *link = [pageLinks objectAtIndex: i]; if ([link hitTest: pdfPosition]) { CGPDFDocumentRef document = CGPDFPageGetDocument(pdfPage); NSObject *linkTarget = [link getLinkTarget: document]; if (linkTarget != nil) { if ([linkTarget isKindOfClass:[NSNumber class]]) { NSNumber *targetPageNumber = (NSNumber *)linkTarget; CGPDFPageRef targetPage = CGPDFDocumentGetPage(document, [targetPageNumber intValue]); if (targetPage != NULL) { [self setPage: targetPage]; [self setNeedsDisplay]; } } else { if ([linkTarget isKindOfClass: [NSString class]]) { NSString *linkUri = (NSString *)linkTarget; NSURL *url = [NSURL URLWithString: linkUri]; [[UIApplication sharedApplication] openURL: url]; } } } break; } }
Finding the link target is the tricky part. The destination of a link is defined as an array or as a string. If it is an array then the first element being a reference to the target page. This destination array can appear directly in the "Dest" entry in the annotation dictionary or it can appear in the "D" entry in the action dictionary associated with the annotation if it is a GoTo action. If it is a URI action, the URL is located in the URI entry of the action dictionary. The annotation action, whether it is a GoTo action or a URI action, is defined by the "A" entry in the annotation dictionary. In the same time, the "Dest" entry can contain a string. In this situation the actual destination array is located in the "Dests" tree in the document catalog.
The getLinkTarget:
method in PDFLinkAnnotation
class handles the computing of the link target. If it returns a NSNumber
, we have a link to another page in the document, if it returns a NSString
we have a link to a URL.
How it works: first it checks the "A" entry in the annotation dictionary. If we have a GoTo action, the "D" entry is verified. It can contain either a destination array or a destination name. If we have a destination name, the "Dests" tree is searched for the corresponding destination array. If we have a URI action, the target URL is retrieved from the URI entry. If "A" entry is not present, the "Dest" entry is checked. If it contains the destination array, we're fine. Otherwise if it contains a string the "Dests" tree in the document catalog is searched for the destination array.
Once we have the destination array we retrieve the target page dictionary from the first element of the array. Unfortunately the page dictionary is not a match to a CGPDFPageRef
, a plain cast from CGPDFDictionaryRef
to CGPDFPageRef
does not work, so we have to loop through all the pages in the document, get the dictionary for each page and test it against our target page dictionary. When we have a match we have found the target page number.
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | - (NSObject*)getLinkTarget:(CGPDFDocumentRef)document { NSObject *linkTarget = nil; CGPDFArrayRef destArray = NULL; CGPDFStringRef destName = NULL; CGPDFDictionaryRef actionDictionary = NULL; if (CGPDFDictionaryGetDictionary(annotationDictionary, "A", &actionDictionary)) { const char* actionType; if (CGPDFDictionaryGetName(actionDictionary, "S", &actionType)) { if (strcmp(actionType, "GoTo") == 0) { // D entry can be either a string object or an array object. if (!CGPDFDictionaryGetArray(actionDictionary, "D", &destArray)) { CGPDFDictionaryGetString(actionDictionary, "D", &destName); } } if (strcmp(actionType, "URI") == 0) { CGPDFStringRef uriRef = NULL; if (CGPDFDictionaryGetString(actionDictionary, "URI", &uriRef)) { const char *uri = (const char *)CGPDFStringGetBytePtr(uriRef); linkTarget = [[NSString alloc] initWithCString: uri encoding: NSASCIIStringEncoding]; } } } } else { // Dest entry can be either a string object or an array object. if (!CGPDFDictionaryGetArray(annotationDictionary, "Dest", &destArray)) { CGPDFDictionaryGetString(annotationDictionary, "Dest", &destName); } } if (destName != NULL) { // Traverse the Dests tree to locate the destination array. CGPDFDictionaryRef catalogDictionary = CGPDFDocumentGetCatalog(document); CGPDFDictionaryRef namesDictionary = NULL; if (CGPDFDictionaryGetDictionary(catalogDictionary, "Names", &namesDictionary)) { CGPDFDictionaryRef destsDictionary = NULL; if (CGPDFDictionaryGetDictionary(namesDictionary, "Dests", &destsDictionary)) { const char *destinationName = (const char *)CGPDFStringGetBytePtr(destName); destArray = [self findDestinationByName: destinationName inDestsTree: destsDictionary]; } } } if (destArray != NULL) { int targetPageNumber = 0; // First entry in the array is the page the links points to. CGPDFDictionaryRef pageDictionaryFromDestArray = NULL; if (CGPDFArrayGetDictionary(destArray, 0, &pageDictionaryFromDestArray)) { int documentPageCount = CGPDFDocumentGetNumberOfPages(document); for (int i = 1; i <= documentPageCount; i++) { CGPDFPageRef page = CGPDFDocumentGetPage(document, i); CGPDFDictionaryRef pageDictionaryFromPage = CGPDFPageGetDictionary(page); if (pageDictionaryFromPage == pageDictionaryFromDestArray) { targetPageNumber = i; break; } } } else { // Some PDF generators use incorrectly the page number as the first element of the array // instead of a reference to the actual page. CGPDFInteger pageNumber = 0; if (CGPDFArrayGetInteger(destArray, 0, &pageNumber)) { targetPageNumber = pageNumber + 1; } } if (targetPageNumber > 0) { linkTarget = [[NSNumber alloc] initWithInt: targetPageNumber]; } } [linkTarget autorelease]; return linkTarget; } |
- (NSObject*)getLinkTarget:(CGPDFDocumentRef)document { NSObject *linkTarget = nil; CGPDFArrayRef destArray = NULL; CGPDFStringRef destName = NULL; CGPDFDictionaryRef actionDictionary = NULL; if (CGPDFDictionaryGetDictionary(annotationDictionary, "A", &actionDictionary)) { const char* actionType; if (CGPDFDictionaryGetName(actionDictionary, "S", &actionType)) { if (strcmp(actionType, "GoTo") == 0) { // D entry can be either a string object or an array object. if (!CGPDFDictionaryGetArray(actionDictionary, "D", &destArray)) { CGPDFDictionaryGetString(actionDictionary, "D", &destName); } } if (strcmp(actionType, "URI") == 0) { CGPDFStringRef uriRef = NULL; if (CGPDFDictionaryGetString(actionDictionary, "URI", &uriRef)) { const char *uri = (const char *)CGPDFStringGetBytePtr(uriRef); linkTarget = [[NSString alloc] initWithCString: uri encoding: NSASCIIStringEncoding]; } } } } else { // Dest entry can be either a string object or an array object. if (!CGPDFDictionaryGetArray(annotationDictionary, "Dest", &destArray)) { CGPDFDictionaryGetString(annotationDictionary, "Dest", &destName); } } if (destName != NULL) { // Traverse the Dests tree to locate the destination array. CGPDFDictionaryRef catalogDictionary = CGPDFDocumentGetCatalog(document); CGPDFDictionaryRef namesDictionary = NULL; if (CGPDFDictionaryGetDictionary(catalogDictionary, "Names", &namesDictionary)) { CGPDFDictionaryRef destsDictionary = NULL; if (CGPDFDictionaryGetDictionary(namesDictionary, "Dests", &destsDictionary)) { const char *destinationName = (const char *)CGPDFStringGetBytePtr(destName); destArray = [self findDestinationByName: destinationName inDestsTree: destsDictionary]; } } } if (destArray != NULL) { int targetPageNumber = 0; // First entry in the array is the page the links points to. CGPDFDictionaryRef pageDictionaryFromDestArray = NULL; if (CGPDFArrayGetDictionary(destArray, 0, &pageDictionaryFromDestArray)) { int documentPageCount = CGPDFDocumentGetNumberOfPages(document); for (int i = 1; i <= documentPageCount; i++) { CGPDFPageRef page = CGPDFDocumentGetPage(document, i); CGPDFDictionaryRef pageDictionaryFromPage = CGPDFPageGetDictionary(page); if (pageDictionaryFromPage == pageDictionaryFromDestArray) { targetPageNumber = i; break; } } } else { // Some PDF generators use incorrectly the page number as the first element of the array // instead of a reference to the actual page. CGPDFInteger pageNumber = 0; if (CGPDFArrayGetInteger(destArray, 0, &pageNumber)) { targetPageNumber = pageNumber + 1; } } if (targetPageNumber > 0) { linkTarget = [[NSNumber alloc] initWithInt: targetPageNumber]; } } [linkTarget autorelease]; return linkTarget; }
Source code: LinksNavigation
[relatedPosts]
Related
Recent Posts
- What’s the resolution of pdf files?
- 5 apps to debug PDF files
- Text extraction from PDF files – part 1
- IFXPDFFactory – part 11 – Document information and viewer preferences
- IFXPDFFactory – part 10 – TrueType fonts in PDF files
Recent Comments
- Dario on PDF Frameworks and Tools for iOS and Mac OS X
- iPDFdev on About
- Grant on About
- iPDFdev on Convert an image to PDF on the iPhone and iPad
- Mike LIma on Convert an image to PDF on the iPhone and iPad
July 15th, 2011 - 18:50
Great tutorial! Thanks!
Do you know how to handle embedded videos as well?
July 17th, 2011 - 09:05
There are a few ways to embed video files in a PDF document. If you can send me a sample PDF file I can show you how to extract the videos.
April 9th, 2014 - 09:08
Would be also interesting for me.
July 26th, 2011 - 13:06
Would it be possible to have the PDF go from edge to edge of the screen instead of centering it? You might loose some of top and bottom of the PDF document but in my case that’s not really an issue.
July 26th, 2011 - 18:39
The code centers the PDF page in the given rectangle in order to keep the original page aspect ratio. This is done by computing the scaling factors for X and Y and considering the minimum between them. You can modify the code and use different scaling factors for X and Y and in this way the page will always fill the given rectangle, but the original aspect ratio will no longer be preserved. Or you can modify the code and use the maximum scaling between X and Y, in this situation the given rectangle will be always filled, the original aspect ratio will be preserved but parts of the page might get cut, either on horizontal or on vertical.
July 27th, 2011 - 00:31
Hello
I’m watching your tutorial and I want to implement in a library that reads pdfs, and I have my doubts whether it can be compatible you can give me your opinion of this library is located at the following address.
http://www.vfr.org/
Thanks
July 27th, 2011 - 17:45
I looked over the library briefly so I cannot tell yet how difficult would be to integrate this code with the library. Maybe the developer of the library can help you with the integration.
August 26th, 2011 - 19:48
Version 2.1 of the code (at https://github.com/vfr/Reader) supports PDF links.
August 26th, 2011 - 21:15
Excellent!
August 16th, 2011 - 05:58
I tried searching your blog for a tutorial on how to enable anotations on pdf’s. Is there a way to enable this ?
It seems that I would have to handle events such as touch events and present the user with the ability to enter text and save it in that part of the pdf, perhaps integrating it as a picture?
any thoughts, suggestions ?
August 16th, 2011 - 15:41
The CGPDF API does not support adding annotations to a PDF file. You need to use a 3rd party PDF library for working with annotations in a PDF file.
September 9th, 2011 - 21:56
Can you link to a couple? The things I’ve seen reference reading annotations but not writing them…
I can’t believe I’ll have to rewrite half my app just to add one feature. And the company thought it’d be “free” to have me write it by myself… Joke’s on them huh?
August 17th, 2011 - 00:12
Thanks for the code! What would be incredible and you seem to really know how to do this… is example code on how to parse the Outlines and get the page number. It seems that only a handful of people know how to do this. So basically parse and extract the table of contents and return a listing with page numbers. That would be incredible!
August 17th, 2011 - 19:07
I know how to extract the outlines from a PDF file and the target page and I plan an article on this matter but unfortunately I do not know when I’ll have the time to write it. The outlines use the same A/Dest entry like the link annotations so once you got this you can retrieve the target page using the code in this article. I’ll let you know when the article about outlines is published.
September 9th, 2011 - 22:39
I tried to post a comment regarding “3rd party PDF libraries” but didn’t see a “you need to be approved” message so I’m assuming it simply didn’t submit.
Can you list a couple 3rd party libraries for adding annotations? The things I’m seeing don’t mention actually adding the annotations, just reading them.
September 14th, 2011 - 18:30
At this moment I do not know any library that can add annotations to a PDF file on iOS. I’ve heard some mentions about libHaru, but I did not test it myself.
September 16th, 2011 - 17:41
Thanks for the reply. I’ll have to check that out. I did find a couple libraries that can handle it. The makers of iAnnotate and the makers of Readdle/PDF Expert both license their libraries. I’m just not sure I can get my company to pay as much as they cost for an app used by 10 people internally.
May 22nd, 2012 - 22:19
Could you please tell me how to handle image annotations on a PDF in iOS. Specifically I need to know how to display drawings that a user my make on a PDF when using Adobe Reader on iPhone or iPad.
May 23rd, 2012 - 15:04
Can you send me a sample PDF file with annotations so I can take a look at it? My email address is in the about page.
May 23rd, 2012 - 13:41
i am testing your links navigation code with the following PDF documents, its parsing links in a good way but when it draws a yellow rectangle over the link rectangle, its displaced from the original one created by viewer. i think there is a problem with the Points conversion functions, Can you please help out?
please fin the attached document from here
http://dl.dropbox.com/u/6137862/1.pdf
and another is
http://dl.dropbox.com/u/6137862/scam0103.pdf
May 23rd, 2012 - 15:10
I’ll look at the files and I’ll get back to you. Where are the links in the 2nd document (scam0103.pdf) because I could not find any?
May 23rd, 2012 - 22:01
thanks for your quick reply, yes its in the bottom of first page string titled “Happy New Year!”
May 25th, 2012 - 20:39
Hi Sorin, did you get any idea of failing the Points conversion for PDFs i have given you the links, i have adapted your code into my application to show parse and show annotations, i am just stucked on the Points conversion from PDF to view.
I am using Poppler to get all the information about links and Text searching, Poppler is giving me this information accurately as i have tested your provided document in sample app as well.
I need your help to translate these PDF Points to View Points to show the information accurately. I have downloaded some test documents from adobe’s provided test data, there is a list of documents which is failing with your solution. “scam0103.pdf” is one of them.
thanks for a wonderful class to render PDF pages 🙂
May 26th, 2012 - 11:23
I found the problem and the updated code is available on the website.
The problem was located in convertPDFPointToViewPoint: method, in the switch block, case 0 section replace this line:
viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - pdfPoint.y) / cropBox.size.height;
with this line:
viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - (pdfPoint.y - cropBox.origin.y)) / cropBox.size.height;
and the yellow rectangles will be drawn at the correct locations.
May 27th, 2012 - 01:32
thanks a lot Sorin for your help its working fine on these documents.
the project you have updated is crashing due to the wrong file URL, please correct it 🙂
i will test it with some other documents, i wish i could find you in case of any problem 🙂
i must say you thanks again for this.
June 21st, 2012 - 10:22
I want to use hyperlink display code in the Leaves PDF reader application. I have try but i am not able to added the code in correct position on the leaves pdf reader. Please help me.
June 22nd, 2012 - 10:49
You have to locate the correct position of the PDF page (page bounding rectangle) in the view (view coordinates) and use this rectangle with the conversion method from PDF units to view units. Locate the piece of code where the PDF page is actually drawn in the view and at that point you should have the page bounding rectangle. This is just a guess because I do not know your PDF reader.
September 17th, 2012 - 10:54
When I run this code on the Xcode 4.5 Simulator I get the below error
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextSetRGBFillColor: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextFillRects: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextSaveGState: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextTranslateCTM: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextScaleCTM: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextScaleCTM: invalid context 0x0
Sep 17 11:52:17 waleed.local LinksNavigation[18031] : CGContextRestoreGState: invalid context 0x0
2012-09-17 11:52:17.989 LinksNavigation[18031:11303] *** Terminating app due to uncaught exception ‘CALayerInvalidGeometry’, reason: ‘CALayer position contains NaN: [nan nan]’
*** First throw call stack:
(0x18c3012 0x15dae7e 0x18c2deb 0x11bfe0b 0x11bffa5 0x11c06eb 0x43a4a3 0x5037d0 0x503a2a 0x35b1 0x3ed8 0x4d9648 0x4d9882 0x428a25 0x429311 0x213f 0x3f57b7 0x3f5da7 0x3f6fab 0x408315 0x40924b 0x3facf8 0x21fedf9 0x21fead0 0x1838bf5 0x1838962 0x1869bb6 0x1868f44 0x1868e1b 0x3f67da 0x3f865c 0x1cb9 0x1bf5)
libc++abi.dylib: terminate called throwing an exception
Any ideas ?
September 17th, 2012 - 11:36
Your file might contain a zero width/height link. Please send me the file (see About page) and I’ll take a look at it.
October 10th, 2012 - 02:18
I get this same error just running your project using your PDF… Running on Xocde 4.5.1 and tried 5.0, 5.1 and 6.0 iPhone and iPad simulators. Any ideas ?
October 10th, 2012 - 18:36
I just ran the project and everything worked fine, both in the simulator and on the iPad. If you execute the code step by step at what line do you receive the error?
February 13th, 2013 - 16:08
I get the same error just running the project, without changing a thing. XCode 4.6. Tried on the 5.0 and 6.1 simulator.
February 13th, 2013 - 16:12
The problem was that the project had the URL set to scam0103.pdf instead of Links.pdf
February 14th, 2013 - 21:14
Thanks, I’ll verify the sample project and fix it.
March 12th, 2013 - 09:59
The problem in the code is in the file PDFScrollView.m at line 68 which refers to a non-existent PDF document in the bundle. Change the line:
NSURL *pdfURL = [[NSBundle mainBundle] URLForResource:@”scam0103.pdf” withExtension:nil];
to use the correct document name:
NSURL *pdfURL = [[NSBundle mainBundle] URLForResource:@”Links.pdf” withExtension:nil];
December 20th, 2012 - 08:30
Hi Sorin , this is really a good article .Among widget annotations is there any way we can find type of Widget like is it a textbox or checkbox . Any idea on how to get this information.
thanks,
-Siva
December 20th, 2012 - 18:22
For widget annotations check the ‘/FT’ entry in the annotation dictionary. It can be ‘/Tx’ for textbox field, ‘/Ch’ for choice fields (dropdowns and listboxes), ‘/Btn’ for buttons (pushbuttons, radiobuttons and checkboxes) and ‘/Sig’ for signature fields. The difference between types of buttons is done using the ‘/Ff’ entry which is a bit mask, you can read more in the PDF spec. If the ‘/FT’ entry is missing, check the ‘/Parent’ entry and retrieve its value which should be a dictionary and check the ‘/FT’ entry in this dictionary.
December 21st, 2012 - 08:00
Thanks Sorin , I will try your suggestions
February 6th, 2013 - 08:26
Hi man, Thank you for your tutorial and I help me to solve opening url in pdf file problem.Also, do you know how to do the search text feature, also need to highlight the searched result ? Thank you again.
February 6th, 2013 - 21:05
The PDFKitten on GitHub project should help you with text search.