3
votes

I am trying to find all hyperlinks on a page in a PDF document and place a UIButton over them.

I have found and played around with the script below which should be able to do it. It finds the hyperlinks and places UIButtons bt they are not on top of the links. Can anyone tell me why that may be?

I have attached an image to show how the buttons are off. The uppermost buttons should be on top of the blue "METROPOLITAN" text and the lowermost buttons should be on top of the text saying "SÆSONEN 2008-2009".

This is the code I am using to find the links and place the buttons.

    NSMutableArray *rectArrays;
    NSMutableArray *storeURLs;

    CGPDFPageRef pdfPage = CGPDFDocumentGetPage(_pdf, pageNumber);
    CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(pdfPage);

    CGPDFArrayRef outputArray;
    if(!CGPDFDictionaryGetArray(pageDictionary, "Annots", &outputArray)) {
        //break;
    }

    int arrayCount = 0;
    arrayCount = CGPDFArrayGetCount(outputArray);
    if(arrayCount > 0) {
        for(int j = 0; j < arrayCount; ++j) {
            CGPDFObjectRef aDictObj;
            if(!CGPDFArrayGetObject(outputArray, j, &aDictObj)) {
                return;
            }

            CGPDFDictionaryRef annotDict;
            if(!CGPDFObjectGetValue(aDictObj, kCGPDFObjectTypeDictionary, &annotDict)) {
                return;
            }

            CGPDFDictionaryRef aDict;
            if(!CGPDFDictionaryGetDictionary(annotDict, "A", &aDict)) {
                return;
            }

            CGPDFStringRef uriStringRef;
            if(!CGPDFDictionaryGetString(aDict, "URI", &uriStringRef)) {
                return;
            }

            CGPDFArrayRef rectArray;
            if(!CGPDFDictionaryGetArray(annotDict, "Rect", &rectArray)) {
                return;
            }

            int arrayCount = CGPDFArrayGetCount( rectArray );
            CGPDFReal coords[4];
            for(int k = 0; k < arrayCount; ++k) {
                CGPDFObjectRef rectObj;
                if(!CGPDFArrayGetObject(rectArray, k, &rectObj)) {
                    return;
                }

                CGPDFReal coord;
                if(!CGPDFObjectGetValue(rectObj, kCGPDFObjectTypeReal, &coord)) {
                    return;
                }

                coords[k] = coord;
            }

            char *uriString = (char *)CGPDFStringGetBytePtr(uriStringRef);

            NSString *uri = [NSString stringWithCString:uriString encoding:NSUTF8StringEncoding];

            CGRect rect = CGRectMake(coords[0], coords[1], coords[2], coords[3]);

            NSLog(@"Found: %f ; %f => %f x %f", coords[0], coords[1], coords[2], coords[3]);

            CGPDFInteger pageRotate = 0;
            CGPDFDictionaryGetInteger(pageDictionary, "Rotate", &pageRotate); 
            CGRect pageRect = CGRectIntegral( CGPDFPageGetBoxRect(pdfPage, kCGPDFMediaBox));
            if(pageRotate == 90 || pageRotate == 270) {
                CGFloat temp = pageRect.size.width;
                pageRect.size.width = pageRect.size.height;
                pageRect.size.height = temp;
            }

            rect.size.width = rect.size.width - rect.origin.x;
            rect.size.height = rect.size.height - rect.origin.y;

            CGAffineTransform trans = CGAffineTransformIdentity;
            trans = CGAffineTransformTranslate(trans, 0, pageRect.size.height);
            trans = CGAffineTransformScale(trans, 1.0, -1.0);

            rect = CGRectApplyAffineTransform(rect, trans);

            NSURL *url = [NSURL URLWithString:uri];
            [rectArrays addObject:[NSValue valueWithCGRect:rect]];
            [storeURLs addObject:url];

            for(int i = 0; i <= [rectArrays count]; i++) {
                UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
                [button setFrame:rect];
                [button setTitle:@"Link" forState:UIControlStateNormal];
                [[self view] addSubview:button];

                NSLog(@"Added: %f ; %f => %f x %f", button.frame.origin.x, button.frame.origin.y, button.frame.size.width, button.frame.size.height);
            }
        }
    }

enter image description here

EDIT: Trying to adjust for the scale factor but it is not working. The UIButtons are still placed wrong.

CGAffineTransform trans = CGAffineTransformIdentity;
trans = CGAffineTransformTranslate(trans, 0, pageRect.size.height * 1.5);
trans = CGAffineTransformScale(trans, 1.5, -1.5);

EDIT I took a look at the source code which iPDFDev is linking too and it seems that I have got the positioning working now. My buttons are now too long. Does anyone know why this may be? If I substract viewRext.origin.x from viewRect.size.width the buttons are too short.

This is my code now.

    CGPDFPageRef pdfPage = CGPDFDocumentGetPage(_pdf, pageNumber);
    CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(pdfPage);

    CGPDFArrayRef outputArray;
    if(!CGPDFDictionaryGetArray(pageDictionary, "Annots", &outputArray)) {
        //break;
    }

    int arrayCount = 0;
    arrayCount = CGPDFArrayGetCount(outputArray);
    if(arrayCount > 0) {
        for(int j = 0; j < arrayCount; ++j) {
            CGPDFObjectRef aDictObj;
            if(!CGPDFArrayGetObject(outputArray, j, &aDictObj)) {
                return;
            }

            CGPDFDictionaryRef annotDict;
            if(!CGPDFObjectGetValue(aDictObj, kCGPDFObjectTypeDictionary, &annotDict)) {
                return;
            }

            CGPDFDictionaryRef aDict;
            if(!CGPDFDictionaryGetDictionary(annotDict, "A", &aDict)) {
                return;
            }

            CGPDFStringRef uriStringRef;
            if(!CGPDFDictionaryGetString(aDict, "URI", &uriStringRef)) {
                return;
            }

            CGPDFArrayRef rectArray;
            if(!CGPDFDictionaryGetArray(annotDict, "Rect", &rectArray)) {
                return;
            }

            int arrayCount = CGPDFArrayGetCount( rectArray );
            CGPDFReal coords[4];
            for(int k = 0; k < arrayCount; ++k) {
                CGPDFObjectRef rectObj;
                if(!CGPDFArrayGetObject(rectArray, k, &rectObj)) {
                    return;
                }

                CGPDFReal coord;
                if(!CGPDFObjectGetValue(rectObj, kCGPDFObjectTypeReal, &coord)) {
                    return;
                }

                coords[k] = coord;
            }

            char *uriString = (char *)CGPDFStringGetBytePtr(uriStringRef);

            NSString *uri = [NSString stringWithCString:uriString encoding:NSUTF8StringEncoding];

            CGRect rect = CGRectMake(coords[0], coords[1], coords[2], coords[3]);

            CGPoint pdfPoint = CGPointMake(rect.origin.x, rect.origin.y);
            CGPoint viewPoint = CGPointMake(0, 0);

            CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox);

            int rotation = CGPDFPageGetRotationAngle(pdfPage);

            CGRect pageRenderRect;
            switch (rotation) {
                case 90:
                case -270:
                    pageRenderRect = CGRectMake(0, 0, 1024, 768);

                    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:
                    pageRenderRect = CGRectMake(0, 0, 768, 1024);

                    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:
                    pageRenderRect = CGRectMake(0, 0, 1024, 768);

                    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:
                    pageRenderRect = CGRectMake(0, 0, 768, 1024);

                    viewPoint.x = pageRenderRect.size.width * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width;
                    viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - pdfPoint.y) / cropBox.size.height;
                    break;
            }

            viewPoint.x = viewPoint.x + pageRenderRect.origin.x;
            viewPoint.y = viewPoint.y + pageRenderRect.origin.y;

            CGRect viewRect = CGRectMake(viewPoint.x, viewPoint.y, rect.size.width, rect.size.height - rect.origin.y);
            viewRect = CGRectMake(viewRect.origin.x, viewRect.origin.y - viewRect.size.height, viewRect.size.width, viewRect.size.height);

            UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
            [button setFrame:viewRect];
            [button setBackgroundColor:[UIColor greenColor]];
            [button setAlpha:0.65];
            [button setTag:kPDFLinkButton];
            [[self view] addSubview:button];
        }
    }

EDIT: This is my final code. See the answer from iPDFDev for more information.

- (void)getLinksFromPDF:(CGPDFDocumentRef)_pdf withPageNumber:(NSUInteger)pageNumber
{
    CGPDFPageRef pdfPage = CGPDFDocumentGetPage(_pdf, pageNumber);
    CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(pdfPage);

    CGPDFArrayRef outputArray;
    if(!CGPDFDictionaryGetArray(pageDictionary, "Annots", &outputArray)) {
        //break;
    }

    int arrayCount = 0;
    arrayCount = CGPDFArrayGetCount(outputArray);
    if(arrayCount > 0) {
        for(int j = 0; j < arrayCount; ++j) {
            CGPDFObjectRef aDictObj;
            if(!CGPDFArrayGetObject(outputArray, j, &aDictObj)) {
                return;
            }

            CGPDFDictionaryRef annotDict;
            if(!CGPDFObjectGetValue(aDictObj, kCGPDFObjectTypeDictionary, &annotDict)) {
                return;
            }

            CGPDFDictionaryRef aDict;
            if(!CGPDFDictionaryGetDictionary(annotDict, "A", &aDict)) {
                return;
            }

            CGPDFStringRef uriStringRef;
            if(!CGPDFDictionaryGetString(aDict, "URI", &uriStringRef)) {
                return;
            }

            CGPDFArrayRef rectArray;
            if(!CGPDFDictionaryGetArray(annotDict, "Rect", &rectArray)) {
                return;
            }

            int arrayCount = CGPDFArrayGetCount( rectArray );
            CGPDFReal coords[4];
            for(int k = 0; k < arrayCount; ++k) {
                CGPDFObjectRef rectObj;
                if(!CGPDFArrayGetObject(rectArray, k, &rectObj)) {
                    return;
                }

                CGPDFReal coord;
                if(!CGPDFObjectGetValue(rectObj, kCGPDFObjectTypeReal, &coord)) {
                    return;
                }

                coords[k] = coord;
            }

            char *uriString = (char *)CGPDFStringGetBytePtr(uriStringRef);

            NSString *uri = [NSString stringWithCString:uriString encoding:NSUTF8StringEncoding];

            CGPoint lowerLeft = [self convertPDFPointToViewPoint:CGPointMake(coords[0], coords[1])];
            CGPoint upperRight = [self convertPDFPointToViewPoint:CGPointMake(coords[2], coords[3])];

            // This is the rectangle positioned under the link
            CGRect viewRect = CGRectMake(lowerLeft.x, lowerLeft.y, upperRight.x - lowerLeft.x, lowerLeft.y - upperRight.y);

            // Now adjusting the rectangle to be on top of the link
            viewRect = CGRectMake(viewRect.origin.x, viewRect.origin.y - viewRect.size.height, viewRect.size.width, viewRect.size.height);

            NSLog(@"%@", uri);

            UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
            [button setFrame:viewRect];
            [button setBackgroundColor:[UIColor greenColor]];
            [button setAlpha:0.65];
            [button setTag:kPDFLinkButton];
            [[self view] addSubview:button];
        }
    }   
}

- (CGPoint)convertPDFPointToViewPoint:(CGPoint)pdfPoint
{
    CGPoint viewPoint = CGPointMake(0, 0);

    CGPDFPageRef pdfPage = CGPDFDocumentGetPage(pdf, currentPage);

    CGRect cropBox = CGPDFPageGetBoxRect(pdfPage, kCGPDFCropBox);

    int rotation = CGPDFPageGetRotationAngle(pdfPage);

    CGRect pageRenderRect;
    switch (rotation) {
        case 90:
        case -270:
            pageRenderRect = CGRectMake(0, 0, 1024, 768);

            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:
            pageRenderRect = CGRectMake(0, 0, 768, 1024);

            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:
            pageRenderRect = CGRectMake(0, 0, 1024, 768);

            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:
            pageRenderRect = CGRectMake(0, 0, 768, 1024);

            viewPoint.x = pageRenderRect.size.width * (pdfPoint.x - cropBox.origin.x) / cropBox.size.width;
            viewPoint.y = pageRenderRect.size.height * (cropBox.size.height - pdfPoint.y) / cropBox.size.height;
            break;
    }

    viewPoint.x = viewPoint.x + pageRenderRect.origin.x;
    viewPoint.y = viewPoint.y + pageRenderRect.origin.y;

    return viewPoint;
}
2

2 Answers

4
votes

The links are stored in the Annots array in the page dictionary. When you loop through the Annots array, you locate the link annotations by verifying the Subtype key in the annotation dictionary, it must be "Link" (name object). If you have a link annotation, you verify if it has an action stored in the A entry. If it has an action, you verify if it is a web link, the S entry in the action dictionary must have the value "URI" (name object). If you have an URI action, then you retrieve the link from the URI entry in the action dictionary.

The annotation position on the PDF page is stored in the Rect array. These are 4 values for 2 opposite corners, usually lower left x, lower left y, upper right x, upper right y. These values are relative to bottom left corner of page's media box. These values must be transformed in view coordinates based on these variables: 1. position in the view where the page is displayed; 2. size in the view of the displayed page (current scaling factor * PDF page size); 3. page crop box; 4. page rotation.

The conversion method looks like this:

- (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.size.height;
            break;
    }

    viewPoint.x = viewPoint.x + pageRenderRect.origin.x;
    viewPoint.y = viewPoint.y + pageRenderRect.origin.y;

    return viewPoint;
}

where the pageRenderRect is the rectangle in the view where the page is displayed. You can find additional reference source code here: http://ipdfdev.com/2011/06/21/links-navigation-in-a-pdf-document-on-iphone-and-ipad/

1
votes

At a guess, are you perhaps applying some calculated transform to scale the PDF content up to whatever frame you have, but failing to apply it to the button frames? You seem to have hard coded a transform into the code shown that assumes a PDF will be rendered with one source point equal to one destination point, if you're scaling the PDF at all then obviously that won't be the case.

That diagnosis would explain why it looks like those buttons are meant to cover 'Metropolitan' and 'Saesonen 2008-2009' but their coordinate system has been scaled down towards the top left.