使用 CGPDFDocument、CATiledLayer 和 UIPageViewController 做簡單的 PDF 閱讀器(一)

0

最近項(xiàng)目中需要給客戶看一些比較私密的文件雷滚,為了防止盜取和篡改需曾,需要用 PDF 格式,為 app 添加了一個(gè)簡單的 PDF 閱讀模塊祈远。

打開 PDF

iOS 打開 PDF 是非常方便的事情呆万,系統(tǒng)原生支持。

- (BOOL)openPDFFile:(NSString*)filePath{
    NSURL* pdfURL = [NSURL fileURLWithPath:filePath];
    CFURLRef cfpdfURL = CFURLCreateWithFileSystemPath(CFAllocatorGetDefault(), CFStringCreateWithCString(CFAllocatorGetDefault(), pdfURL.path.UTF8String, kCFStringEncodingUTF8), kCFURLPOSIXPathStyle, NO);
    _pdfDocument = CGPDFDocumentCreateWithURL(cfpdfURL);
    CFRelease(cfpdfURL);
    if (_pdfDocument == nil) {
        return NO;
    }
    _totalPageNum = CGPDFDocumentGetNumberOfPages(_pdfDocument);
    return YES;
}
CFStringCreateWithCString(CFAllocatorGetDefault(), pdfURL.path.UTF8String, kCFStringEncodingUTF8)

CGPDFDocumentCreateWithURL這里的 path车份,可以使用 NSURL 的fileURLWithPath:來獲取谋减,并用CFStringCreateWithCString轉(zhuǎn)換。

_pdfDocument 是 CGPDFDocumentRef 類型的扫沼,在打開 PDF 文件以后逃顶,一直持有 PDF 文件的引用。

_totalPageNum = CGPDFDocumentGetNumberOfPages(_pdfDocument);

使用這個(gè)方法獲取 PDF 總頁數(shù)充甚,供翻頁的時(shí)候使用。

- (PDFContentViewController* _Nullable)showPage:(size_t)pageNum{
    if (_totalPageNum > 0 && pageNum <= _totalPageNum && pageNum > 0) {
        if (_currentPage != nil) {
            _currentPage = nil;
        }
        _currentPage = CGPDFDocumentGetPage(_pdfDocument, pageNum);
        PDFContentViewController* pdfContentViewController = [[PDFContentViewController alloc]init];
        [pdfContentViewController showPdfPage:_currentPage];
        pdfContentViewController.pageNumber = pageNum;
        return pdfContentViewController;
    }else{
        return nil;
    }
}

這里使用 CGPDFDocumentGetPage 來獲取 PDF 文件中某一頁的引用霸褒。
第一個(gè)參數(shù)是對 pdf 文件的引用伴找,第二個(gè)參數(shù)可以直接取到某一頁。這里很奇快的是废菱,page 的下標(biāo)是從1開始的技矮,向CGPDFDocumentGetPage傳入0會(huì)直接報(bào)錯(cuò)。

PDFContentViewController是一個(gè)顯示 PDF 頁面的類殊轴,每個(gè) PDF 頁面創(chuàng)建一個(gè)類衰倦。剛好給 UIPageViewController 做翻頁效果使用。UIPageViewController 用起來很坑旁理,這個(gè)以后再說樊零。

PDF文件顯示

繪制 pdf 文件

繪制 pdf 文件跟平時(shí)繪制圖片差不多

CGSize pdfPageOriginalPageSize = CGPDFPageGetBoxRect(_pdfPage, PDFContentDefaultPDFBox).size;
    CGSize imageSize = CGSizeMake(self.pdfContentView.bounds.size.width, self.pdfContentView.bounds.size.height);
    UIGraphicsBeginImageContext(imageSize);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, self.contentBackground.CGColor);
    CGContextSetStrokeColorWithColor(context, self.contentBackground.CGColor);
    CGContextFillRect(context,CGRectMake(0, 0, imageSize.width, imageSize.height));
    CGContextSaveGState(context);
    CGContextTranslateCTM(context, 0, imageSize.height);
    CGContextScaleCTM(context, 1, -1);
    CGContextScaleCTM(context, imageSize.width/(pdfPageOriginalPageSize.width > 0?pdfPageOriginalPageSize.width:imageSize.width), imageSize.height/(pdfPageOriginalPageSize.height > 0?pdfPageOriginalPageSize.height:imageSize.height));
    CGContextDrawPDFPage(context, _pdfPage);
    CGContextRestoreGState(context);
    UIImage* thumbImg = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    self.pdfContentView.layer.contents = (__bridge id _Nullable)(thumbImg.CGImage);

關(guān)鍵是這里
CGContextDrawPDFPage(context, _pdfPage);
這里繪制的時(shí)候,繪制 context 的 CTM 是相對于 pdf 文件來變換的(跟繪制 Image 到 CGContext 上的變換規(guī)則是一樣的,想象成繪制圖片就好)
如果不變換 CTM 就會(huì)繪制一個(gè)倒立的圖像驻襟。并且夺艰,上面的代碼中,pdf 頁面的大小遠(yuǎn)遠(yuǎn)大于 UIGraphicsBeginImageContext(imageSize) 生成的 Image沉衣,于是就看到一個(gè)倒立的郁副,只繪制了一部分的 pdf 文件。
這段代碼是繪制了一個(gè) Thumbnail Image豌习,所以就沒有繪制高清的圖片存谎,如果需要繪制高清的圖片,例如在 7s plus 上顯示的圖片肥隆,至少要繪制一個(gè) 3x 于屏幕分辨率的圖片既荚,縮小后顯示出來。

CGContextScaleCTM(context, imageSize.width/(pdfPageOriginalPageSize.width > 0?pdfPageOriginalPageSize.width:imageSize.width), imageSize.height/(pdfPageOriginalPageSize.height > 0?pdfPageOriginalPageSize.height:imageSize.height));

繪制的關(guān)鍵在這里巷屿,將整個(gè) pdf 頁面固以,縮放到與即將生成的 image 同樣大小,就可以把整個(gè)頁面都繪制到 image 上面了嘱巾。

在計(jì)算 scale 的時(shí)候憨琳,也可以用

CGPDFPageGetDrawingTransform(_PDFPageRef, kCGPDFCropBox, self.bounds, 0, true)

系統(tǒng)會(huì)根據(jù)第二個(gè)參數(shù)傳入的值,將 pdf 裁剪之后放入第三個(gè)參數(shù)傳入的 Rect 里面旬昭。

CGSize pdfPageOriginalPageSize = CGPDFPageGetBoxRect(_pdfPage, PDFContentDefaultPDFBox)

這個(gè)需要說明一下篙螟,每個(gè) pdf 頁面都有一個(gè)原始大小。雖然 PDF 格式是矢量圖问拘,但是遍略,其中的圖片等內(nèi)容不是矢量的,這個(gè)原始大小也就比較容易理解了骤坐。第二個(gè)參數(shù) CGPDFBox绪杏。系統(tǒng)會(huì)根據(jù)傳入的值來返回一個(gè)裁剪后的 pdf 頁面大小

typedef CF_ENUM (int32_t, CGPDFBox) {
  kCGPDFMediaBox = 0,
  kCGPDFCropBox = 1,
  kCGPDFBleedBox = 2,
  kCGPDFTrimBox = 3,
  kCGPDFArtBox = 4
};

具體的裁剪規(guī)則,還沒有研究清楚纽绍。
此外蕾久,需要知道的是,PDF 里面的每個(gè)元素都是可以讀取到的拌夏,iOS 也給我們提供了相應(yīng)的方法僧著。
如果有打開 pdf 中的 url 之類的需求,可以研究一下這方面的內(nèi)容障簿。

在回到顯示部分盹愚,實(shí)際瀏覽時(shí),如果存在放大縮小滑動(dòng)等動(dòng)作站故,就必須重新繪圖皆怕。用 UIScrollview 的 contentOffset 來計(jì)算當(dāng)前正在顯示的區(qū)域,并繪制到屏幕上。

但是會(huì)產(chǎn)生幾個(gè)問題:1.如果繪制與 UIScrollView 的 ContentSize 同樣大小的圖片端逼,那么朗兵,當(dāng) Scale 非常大的時(shí)候,繪制的圖片也非常大顶滩,最終會(huì)內(nèi)存占用過高而崩潰余掖。
2.手動(dòng)計(jì)算正在顯示的區(qū)域比較困難。

好在 CATiledLayer可以幫我們解決這個(gè)難題礁鲁,但是 CATiledLayer 也有一些坑盐欺。

使用 CATiledLayer 顯示 PDF

上面的繪圖代碼是顯示縮略圖的繪制代碼,由于CATiledLayer 繪制圖片是異步的仅醇,在繪制過程中冗美,會(huì)存在一塊一塊閃爍的情況,在 CATiledLayer 下面放一個(gè)縮略圖析二,效果比較好粉洼。
CATiledLayer是 iOS 提供的一個(gè)專門顯示大圖片的類,它可以分塊加載叶摄,異步加載属韧。并且有自己的內(nèi)存管理機(jī)制,可以把屏幕外的部分及時(shí)移除蛤吓。
在 UIScrollview 上面使用 CATiledLayer 時(shí)需要設(shè)置
levelsOfDetail 和 levelsOfDetailBias宵喂。
levelsOfDetail = 3指的是,UIScrollview 縮放倍數(shù)(scale)在1/2 1/4 1/8時(shí)使用同樣的 scale 重繪屏幕会傲」兀縮小時(shí)節(jié)省內(nèi)存。
levelsOfDetailBias = 3 指的是UIScrollview 縮放倍數(shù)(scale)在2 4 8時(shí)使用同樣的 scale 重繪屏幕淌山。保持清晰度裸燎。
簡直就是為展示 PDF 文件生的。
只要設(shè)置CATiledLayer的 delegate泼疑,并且在

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context

里面繪制就可以了德绿,但是不能手動(dòng)設(shè)置CATiledLayer的 content。
這里就是 delegate 的繪制方法

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context{
        PDFContentViewController* strongSelf = self;
        double screenScale = [UIScreen mainScreen].scale;
        CGSize pdfPageOriginalPageSize = CGPDFPageGetBoxRect(strongSelf->_pdfPage, PDFContentDefaultPDFBox).size;
        CGSize imageSize = CGSizeMake(layer.bounds.size.width, layer.bounds.size.height);
        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
        CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
        CGContextFillRect(context,CGContextGetClipBoundingBox(context));
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, 0, layer.bounds.size.height);
        CGContextScaleCTM(context, 1, -1);
        
        CGContextScaleCTM(context, imageSize.width/(pdfPageOriginalPageSize.width > 0?pdfPageOriginalPageSize.width:imageSize.width), imageSize.height/(pdfPageOriginalPageSize.height > 0?pdfPageOriginalPageSize.height:imageSize.height));
        CGContextDrawPDFPage(context, strongSelf->_pdfPage);
        CGContextRestoreGState(context);
}

需要注意的是:context在傳入這個(gè)方法時(shí)王浴,已經(jīng)做過 CTM 變換了,它的 Clip 和 scale 都可以發(fā)生了改變梅猿。
獲取 scale 使用

CGContextGetCTM(context).a

獲取 clip 使用

CGContextGetClipBoundingBox(context)

通常情況下氓辣,這個(gè) context 描繪的就是當(dāng)前需要繪制的塊,不要改變 scale 和 clip袱蚓〕ィ可以忽略這個(gè)變換,直接在這個(gè)基礎(chǔ)上繼續(xù)變換 context 即可,就好像在一個(gè)普通的 layer 繪制全屏的圖像一樣体斩。

但是這個(gè)地方是多線程的梭稚,如果處理不好,可能會(huì)引用的已經(jīng)釋放的 self 變量絮吵,導(dǎo)致崩潰弧烤。這里使用 weak-strong self 的方式進(jìn)行處理。

20161122211801277-2017215

這本書非常好蹬敲,看過 PDF 之后已入正收藏暇昂。

20161122211953103-2017215

內(nèi)存占用也比較小。

現(xiàn)在已經(jīng)完成了 PDF 文件的打開和繪制一個(gè)頁面伴嗡,下一步就要進(jìn)行縮放和翻頁等操作了

http://www.cocoachina.com/bbs/read.php?tid-48894-keyword-CATiledLayer.html
http://stackoverflow.com/questions/2295151/catiledlayer-drawing-crash
https://developer.apple.com/library/content//qa/qa1637/_index.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末急波,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瘪校,更是在濱河造成了極大的恐慌澄暮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阱扬,死亡現(xiàn)場離奇詭異泣懊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)价认,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門嗅定,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人用踩,你說我怎么就攤上這事渠退。” “怎么了脐彩?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵碎乃,是天一觀的道長。 經(jīng)常有香客問我惠奸,道長梅誓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任佛南,我火速辦了婚禮梗掰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗅回。我一直安慰自己及穗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布绵载。 她就那樣靜靜地躺著埂陆,像睡著了一般苛白。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上焚虱,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天购裙,我揣著相機(jī)與錄音,去河邊找鬼鹃栽。 笑死躏率,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谍咆。 我是一名探鬼主播禾锤,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼摹察!你這毒婦竟也來了恩掷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤供嚎,失蹤者是張志新(化名)和其女友劉穎黄娘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體克滴,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逼争,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劝赔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片誓焦。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖着帽,靈堂內(nèi)的尸體忽然破棺而出杂伟,到底是詐尸還是另有隱情,我是刑警寧澤仍翰,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布赫粥,位于F島的核電站,受9級特大地震影響予借,放射性物質(zhì)發(fā)生泄漏越平。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一灵迫、第九天 我趴在偏房一處隱蔽的房頂上張望秦叛。 院中可真熱鬧,春花似錦瀑粥、人聲如沸挣跋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浆劲。三九已至,卻和暖如春哀澈,著一層夾襖步出監(jiān)牢的瞬間牌借,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工割按, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留膨报,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓适荣,卻偏偏與公主長得像现柠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子弛矛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容