美文网首页
Reader源码解析及PDFKit的简单使用

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

作者: CCChaya | 来源:发表于2018-05-08 22:46 被阅读0次

    前言

    在开发中,时常会遇到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实现

    相关文章

      网友评论

          本文标题:Reader源码解析及PDFKit的简单使用

          本文链接:https://www.haomeiwen.com/subject/fnjdrftx.html