前言
在开发中,时常会遇到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
该类主要是文档管理,包含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.主要流程
在此图中只体现了主要流程,预览功能等并未展示出
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 *)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 *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实现