Reader源码解析及PDFKit的简单使用

前言

在开发中,时常会遇到pdf文档的展示,大多只需要展示pdf文档,但也有不仅能展示还能操作(如iBooks),而iOS 11.0以上(包括)能使用PDFKit框架来轻松的实现此类功能,所以就研究了在这框架之前的一个Reader框架,比较的使用了PDFKit的来实现其功能。

一、PDF文档预览方式

1.使用UIWebview,在现在app开发中,如果仅展示pdf(如协议等的展示),大多都采用此方式

UIWebView *webView = [[UIWebView alloc] init];
//filePath可以是本地的也可以是网络的
NSURLRequest *request = [NSURLRequest requestWithURL: filePath];
//如果还有后续操作实现相应代理方法即可
[webView loadRequest:request];

2.QLPreviewController加载pdf文档,我也只是在网上看到过,没有用过。

3.用CGContext画pdf文档,并结合UIPageViewController展示 Reader框架也是用CGContext实现,以自己封装的VC展示。而PDFKit看不到源码,但实现方式估计离不开CG库(Quartz),而展示方式也可用UIPageViewController展示

4.Reader 实现了加载、展示、书签、打印、发邮件、分享pdf文档等功能

5.PDFKit 提供了已封装好了PDFView、PDFDocument等类,用起来更简单,但仅限于iOS11以上(包括);

二、Reader源码分析

1.主要涉及类

1). ReaderDocument : NSObject <NSObject, NSCoding>
该类主要是文档管理,包含guid(可以理解为文档独特标识号)、fileDate(文档修改时间)、lastOpen(最近打开文档时间)、fileSize(文档大小)、pageCount(总页数)、pageNumber(当前页,默认第一页)、bookmarks(书签、NSMutableIndexSet类型)、password、fileName、fileURL等属性。管理该文档是否可以打印、导出、发邮件等功能。通过NSCodeing保存对象。
2).ReaderViewController 以UIScrollView控件实现了翻页功能,在该类中还实现了打印、分享、发邮件等功能
3).ThumbsViewController 展示pdf预览、pdf书签
4).ReaderContentView 展示pdf的主要View,为ReaderViewController的scrollView的子view。
5).ReaderMainPagebar ReaderContentView展示pdf的缩略图
....

2.主要流程
UML类图.png

在此图中只体现了主要流程,预览功能等并未展示出

3.主要代码解析

1.创建ReaderDocument(文档管理)

1)先判断是否是pdf文件再操作

ReaderDocument *document = [ReaderDocument withDocumentFilePath:filePath password:phrase];
//判断文件是否是pdf文件
+ (BOOL)isPDF:(NSString *)filePath {
    BOOL state = NO;

    if (filePath != nil)  {
        const char *path = [filePath fileSystemRepresentation];

        int fd = open(path, O_RDONLY); // 打开文件

        if (fd > 0) // df>0 打开正常的描述我要符
        {
            const char sig[1024]; // File signature buffer

            ssize_t len = read(fd, (void *)&sig, sizeof(sig));  //写入描述符,成功时返回读的字节数,失败为-1

            state = (strnstr(sig, "%PDF", len) != NULL); //查找是否含有pdf字眼

            close(fd); // Close the file
        }
    }

    return state;
}

2)根据路径及密码创建ReaderDocument,主要用到Quartz提供的CGPDFDocumentRef数据类型来表示PDF文档,Quartz 2D 是Core Graphic框架的一部分,因此其中的很多数据类型和方法都是以CG开头的。

- (instancetype)initWithFilePath:(NSString *)filePath password:(NSString *)phrase {
    if ((self = [super init])) // Initialize superclass first {
        if ([ReaderDocument isPDF:filePath] == YES) // Valid PDF {
            _guid = [ReaderDocument GUID]; // Create document's GUID,创建一个文档GUID

            _password = [phrase copy]; // Keep copy of document password

            _filePath = [filePath copy]; // Keep copy of document file path

            _pageNumber = [NSNumber numberWithInteger:1]; // Start on page one,当前页,默认第一页

            _bookmarks = [NSMutableIndexSet new]; // Bookmarked pages index set

            CFURLRef docURLRef = (__bridge CFURLRef)[self fileURL]; // CFURLRef from NSURL
            CGPDFDocumentRef thePDFDocRef = CGPDFDocumentCreateUsingUrl(docURLRef, _password);

            if (thePDFDocRef != NULL) // Get the total number of pages in the document
            {
                NSInteger pageCount = CGPDFDocumentGetNumberOfPages(thePDFDocRef); //得到文档总页数

                _pageCount = [NSNumber numberWithInteger:pageCount];

                CGPDFDocumentRelease(thePDFDocRef); // CG库,纯C语言,需要手动释放
            }
            else // Cupertino, we have a problem with the document
            {
                NSAssert(NO, @"CGPDFDocumentRef == NULL");
            }

            _lastOpen = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0];

            NSFileManager *fileManager = [NSFileManager defaultManager]; // Singleton

            NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:_filePath error:NULL];

            _fileDate = [fileAttributes objectForKey:NSFileModificationDate]; // File date 文档的最后修改日期

            _fileSize = [fileAttributes objectForKey:NSFileSize]; // File size (bytes)  文档大小

            [self archiveDocumentProperties]; // 归档,保存属性
        }
        else // Not a valid PDF file
        {
            self = nil;
        }
    }

    return self;
}

2.展示pdf文档及系列操作,ReaderViewController
在这个VC中,展示了PDF功能,并且实现了打印、发邮件等功能,有了ReaderDocument后,实现起来就容易了,作者以自己封装的View展示,可通过类图查看其关系。里面涉及PDF内容代码解析一下
1) ReaderViewController跳转

ReaderViewController *readerViewController = [[ReaderViewController alloc] initWithReaderDocument:document];

readerViewController.delegate = self;

2)通过Quartz关于PDF文档生成和PDF元数据访问

CFURLRef docURLRef = (__bridge CFURLRef)[self fileURL]; // 得到CFURLRef路径
CGPDFDocumentRef thePDFDocRef = CGPDFDocumentCreateUsingUrl(docURLRef, _password);//通过路径及密码得到文档信息
if (thePDFDocRef != NULL) {
    NSInteger pageCount = CGPDFDocumentGetNumberOfPages(thePDFDocRef); //得到文档总页数
    CGPDFPageRef  PDFPageRef = CGPDFDocumentGetPage(_PDFDocRef, page); // 得到当前页的索引,可以通过该索引获得该页的一些信息、如旋转角度、Rect信息、文档的第几页(CGPDFPageGetPageNumber)等
    NSInteger pageAngle = CGPDFPageGetRotationAngle(PDFPageRef); // 得到旋转角度,0、90、180、270度
    CGPDFDocumentRelease(thePDFDocRef); // CG库,纯C语言,需要手动释放
}

画pdf,并得到图片

CGPDFPageRef thePDFPageRef = CGPDFDocumentGetPage(thePDFDocRef, page);//得到page信息
CGContextRef context = CGBitmapContextCreate(NULL, target_w, target_h, 8, 0, rgb, bmi);//上下文,大小、位图组成等参数
//CGContextSetRGBFillColor,CGContextFillRect,CGContextConcatCTM  上下文一些基础设置
CGContextDrawPDFPage(context, thePDFPageRef);  //画pdf
imageRef = CGBitmapContextCreateImage(context); //能过上下文,即位图信息得到CGImageRef
UIImage *image = [UIImage imageWithCGImage:imageRef scale:request.scale orientation:UIImageOrientationUp]; //得到图片

获取PDF页上的超链接,包括目录链接及url跳转链接,Reader在ReaderContentPage实现,如果对索引结构有疑问,可以查看这篇文章 ,目录信息结构图讲的很清楚

//建立链接信息,作者还在链接中调整了角度问题,在这里未给出源码
- (void)buildAnnotationLinksList {
    _links = [NSMutableArray new]; // Links list array
    CGPDFArrayRef pageAnnotations = NULL; // Page annotations array,创建一个装链接的数组

    CGPDFDictionaryRef pageDictionary = CGPDFPageGetDictionary(_PDFPageRef);//PDFPageRef 当前页索引,在上个代码块中获得
//在字典中找key-"Annots"获得链接数组,并存于pageAnnotations中,以下该类方法都类似,就是去字典中找key,找到了value放入参数中
    if (CGPDFDictionaryGetArray(pageDictionary, "Annots", &pageAnnotations) == true)
    {
        NSInteger count = CGPDFArrayGetCount(pageAnnotations); // 得到个数
        for (NSInteger index = 0; index < count; index++)  //遍历{
            CGPDFDictionaryRef annotationDictionary = NULL; // PDF annotation dictionary
//将当前的链接信息放至annotationDictionary中
            if (CGPDFArrayGetDictionary(pageAnnotations, index, &annotationDictionary) == true) {
                const char *annotationSubtype = NULL; // PDF annotation subtype string

                if (CGPDFDictionaryGetName(annotationDictionary, "Subtype", &annotationSubtype) == true)
                {
                    if (strcmp(annotationSubtype, "Link") == 0) // Found annotation subtype of 'Link'
                    {
                        ReaderDocumentLink *documentLink = [self linkFromAnnotation:annotationDictionary];

                        if (documentLink != nil) [_links insertObject:documentLink atIndex:0]; // Add link
                    }
                }
            }
        }

        //[self highlightPageLinks]; // Link support debugging
    }
}

//当点击了链接信息
- (id)processSingleTap:(UITapGestureRecognizer *)recognizer
{
    id result = nil; // Tap result object

    if (recognizer.state == UIGestureRecognizerStateRecognized)
    {
        if (_links.count > 0) // Process the single tap
        {
            CGPoint point = [recognizer locationInView:self];

            for (ReaderDocumentLink *link in _links) // Enumerate links
            {
                if (CGRectContainsPoint(link.rect, point) == true) // Found it
                {
//当点击在链接上,返回target给ReaderViewController来控制跳转
                    result = [self annotationLinkTarget:link.dictionary]; break;
                }
            }
        }
    }

    return result;
}

- (id)annotationLinkTarget:(CGPDFDictionaryRef)annotationDictionary {
    id linkTarget = nil; // Link target object

    CGPDFStringRef destName = NULL; const char *destString = NULL;

    CGPDFDictionaryRef actionDictionary = NULL; CGPDFArrayRef destArray = NULL;
//目录信息有的是用/A作索引,然后在/D下面找到page对象就好了,有的是/Dest作索引
    if (CGPDFDictionaryGetDictionary(annotationDictionary, "A", &actionDictionary) == true)
    {
        const char *actionType = NULL; // Annotation action type string

        if (CGPDFDictionaryGetName(actionDictionary, "S", &actionType) == true)
        {
            if (strcmp(actionType, "GoTo") == 0) // GoTo action type
            {
                if (CGPDFDictionaryGetArray(actionDictionary, "D", &destArray) == false)
                {
                    CGPDFDictionaryGetString(actionDictionary, "D", &destName);
                }
            }
            else // Handle other link action type possibility
            {
                if (strcmp(actionType, "URI") == 0) // URI action type
                {
                    CGPDFStringRef uriString = NULL; // Action's URI string

                    if (CGPDFDictionaryGetString(actionDictionary, "URI", &uriString) == true)
                    {
                        const char *uri = (const char *)CGPDFStringGetBytePtr(uriString); // Destination URI string

                        NSString *target = [NSString stringWithCString:uri encoding:NSUTF8StringEncoding]; // NSString - UTF8

                        linkTarget = [NSURL URLWithString:[target stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];

                        if (linkTarget == nil) NSLog(@"%s Bad URI '%@'", __FUNCTION__, target);
                    }
                }
            }
        }
    }
    else // Handle other link target possibilities
    {
        if (CGPDFDictionaryGetArray(annotationDictionary, "Dest", &destArray) == false)
        {
            if (CGPDFDictionaryGetString(annotationDictionary, "Dest", &destName) == false)
            {
                CGPDFDictionaryGetName(annotationDictionary, "Dest", &destString);
            }
        }
    }

    if (destName != NULL) // Handle a destination name {

//获取pdf的元信息,目录信息就放在里面,需要自己解析CGPDFDocumentRef _PDFDocRef
        CGPDFDictionaryRef catalogDictionary = CGPDFDocumentGetCatalog(_PDFDocRef);

        CGPDFDictionaryRef namesDictionary = NULL; // Destination names in the document

        if (CGPDFDictionaryGetDictionary(catalogDictionary, "Names", &namesDictionary) == true)
        {
            CGPDFDictionaryRef destsDictionary = NULL; // Document destinations dictionary

            if (CGPDFDictionaryGetDictionary(namesDictionary, "Dests", &destsDictionary) == true)
            {
                const char *destinationName = (const char *)CGPDFStringGetBytePtr(destName); // Name
//能过以下方法得到数组,可去看源代码,该方法未在此体现
                destArray = [self destinationWithName:destinationName inDestsTree:destsDictionary];
            }
        }
    }

    if (destString != NULL) // Handle a destination string
    {
        CGPDFDictionaryRef catalogDictionary = CGPDFDocumentGetCatalog(_PDFDocRef);

        CGPDFDictionaryRef destsDictionary = NULL; // Document destinations dictionary

        if (CGPDFDictionaryGetDictionary(catalogDictionary, "Dests", &destsDictionary) == true)
        {
            CGPDFDictionaryRef targetDictionary = NULL; // Destination target dictionary

            if (CGPDFDictionaryGetDictionary(destsDictionary, destString, &targetDictionary) == true)
            {
                CGPDFDictionaryGetArray(targetDictionary, "D", &destArray);
            }
        }
    }

    if (destArray != NULL) // Handle a destination array
    {
        NSInteger targetPageNumber = 0; // The target page number

        CGPDFDictionaryRef pageDictionaryFromDestArray = NULL; // Target reference

        if (CGPDFArrayGetDictionary(destArray, 0, &pageDictionaryFromDestArray) == true)
        {
            NSInteger pageCount = CGPDFDocumentGetNumberOfPages(_PDFDocRef); // Pages

            for (NSInteger pageNumber = 1; pageNumber <= pageCount; pageNumber++)
            {
                CGPDFPageRef pageRef = CGPDFDocumentGetPage(_PDFDocRef, pageNumber);

                CGPDFDictionaryRef pageDictionaryFromPage = CGPDFPageGetDictionary(pageRef);

                if (pageDictionaryFromPage == pageDictionaryFromDestArray) // Found it
                {
                    targetPageNumber = pageNumber; break;
                }
            }
        }
        else // Try page number from array possibility
        {
            CGPDFInteger pageNumber = 0; // Page number in array

            if (CGPDFArrayGetInteger(destArray, 0, &pageNumber) == true)
            {
                targetPageNumber = (pageNumber + 1); // 1-based
            }
        }

        if (targetPageNumber > 0) // We have a target page number
        {
            linkTarget = [NSNumber numberWithInteger:targetPageNumber];
        }
    }

    return linkTarget;
}

三、PDFKit的简单使用

1.PDFKit相关类

PDFKit的相关类不多,简单易用,实质就是对Quartz中的关于PDF模块的一个封装,刚刚看了Reader源码,对其就更好了解了。相关类如下:

1、PDFDocument: 代表一个PDF文档,可以使用初始化方法-initWithURL;包含了文档一些基本属性、如pageCount(页面数),是否锁定、加密,可否打印、复制,提供增删查改某页、查找内容等功能。如果需要文档修改时间、大小、书签可以借鉴Reader对其封装。

self.pdfDocument = [[PDFDocument alloc] initWithURL:url];

2、PDFView: 呈现PDF文档的UIView,包括一些文档操作(如链接跳转、页面跳转、选中),可使用-initWithDocument:方法进行初始化,也可用-initWithFrame:;可以设置其展示样式。

- (PDFView *)pdfView {
    
    if (!_pdfView) {
        _pdfView = [[PDFView alloc] initWithFrame:CGRectMake(0, kTOOLBAR_HEIGHT + kSTATUS_HEIGHT, kScreenWidth, kScreenHeight - kTOOLBAR_HEIGHT - kSTATUS_HEIGHT - 60)];
        _pdfView.autoScales = YES;   //自动适应尺寸
//        _pdfView.displayMode = kPDFDisplaySinglePageContinuous;  // 默认是这个模式
        _pdfView.displayDirection = kPDFDisplayDirectionHorizontal;
        
        _pdfView.delegate = self;
//        _pdfView.interpolationQuality = kPDFInterpolationQualityHigh;
//        _pdfView.displaysAsBook = YES;
        _pdfView.document = self.pdfDocument;
        [_pdfView usePageViewController:YES withViewOptions:nil];
    }
    return _pdfView;
}

3、 PDFThumbnailView: 这个类是一个关于PDF的缩略视图。通过设置其PDFView属性来关联一个PDFView

- (PDFThumbnailView *)thumbnailView {
    
    if (!_thumbnailView) {
        _thumbnailView = [[PDFThumbnailView alloc] initWithFrame:CGRectMake(0, kScreenHeight - 45, kScreenWidth, 45)];
        _thumbnailView.thumbnailSize = CGSizeMake(20, 25); //设置size
        _thumbnailView.backgroundColor = [UIColor whiteColor];
        _thumbnailView.PDFView = self.pdfView;
        _thumbnailView.layoutMode = PDFThumbnailLayoutModeHorizontal;
    }
    return _thumbnailView;
}

4、 PDFPage: 表示了当前PDF文档中的一页,有label属性来代表是第几页、rotation(旋转角度)、NSArray< PDFAnnotation *>本页中的一些备注信息等;

PDFPage *pdfPage = [self.pdfDocument pageAtIndex:indexPath.item] ;
UIImage *pdfImage = [pdfPage thumbnailOfSize:cell.bounds.size forBox:kPDFDisplayBoxCropBox];

5、 PDFOutline: 表示了整个PDF文档的轮廓,比如有些带目录标签的文档

- (NSMutableArray<PDFOutline *> *)dirArray {
    
    if (!_dirArray) {
        _dirArray = [NSMutableArray array];
        for (NSInteger index = 0; index < self.pdfDocument.outlineRoot.numberOfChildren; index++) {
            PDFOutline *outLine = [self.pdfDocument.outlineRoot childAtIndex:index];
            [_dirArray addObject:outLine];
        }
    }
    return _dirArray;
}

6、 PDFAnnotation: 表示了PDF文档中加入的一些标注,如下划线,删除线,备注等等。

//可以自定义UIMenuController中的UIMenuItem来实现笔记功能

PDFAnnotation *annotation = [[PDFAnnotation alloc] initWithBounds:CGRectMake(100, 100, 100, 10) forType:PDFAnnotationSubtypeCircle withProperties:@{PDFAnnotationKeyColor:[UIColor redColor]}];
    annotation.shouldDisplay = YES;
    [self.pdfView.currentPage addAnnotation:annotation];

7、 PDFSelection:表示了PDF文档中的一个选区,string属性代表选区内容

NSArray<PDFSelection *>  *array = [self.pdfDocument findString:@"12" withOptions:NSWidthInsensitiveSearch];
    for (PDFSelection *selection in array) {
        NSLog(@"selection = %@",selection.string);
    }

8、 PDFAction: 表示了PDF文档中的一个动作,比如点击一个链接等等

//当点击了目录信息
- (void)ReaderThumbnailGridVCDelegateVC:(ReaderThumbnailGridVC *)VC DidSelectDirectory:(PDFOutline *)pdfOutLine {
    PDFAction *action = pdfOutLine.action; //也可用PDFDestination实现
    [self.pdfView performAction:action];
}

9、PDFKitPlatformView:宏定义

写了个简单Demo,可供参考
另像书签功能、PDFAnnotation都未包含在里面,大家可参考Reader实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容