前言
在開發中,時常會遇到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.主要流程
在此圖中只體現了主要流程,預覽功能等并未展示出
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:宏定義