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閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容