pdf閱讀器開發


文章基於sumatrapdf的實現(當中mupdf中的內容不會太多涉及)。以及自己在此基礎上做的
優化,擴展。詳細效果能夠參考百度閱讀器精簡版。




最NB的還是得屬於foxit。渲染速度一流,展示大圖片時非常快。




第一部分:PDF基礎


第二部分:PDF功能實現


1.展示模式和坐標變換
pdf原生支持一些展示模式,在sumatrapdf的實現中又有一些展示模式,能夠實現
pdf原生支持的這些模模式,並在此基礎上擴展出一些展示模式。


而模式大概分為兩類:
一類模式是有一個虛擬的Canvas。每個頁面一行一行地排列在上面,每行可能有一個,兩個,甚至
多個頁面。Canvas有上下左右邊距,頁面間有水平和垂直的邊距。這個時候,全部的頁面都處於
可見狀態。然后使用一個矩形框,矩形的邊平行於Canvas的邊,矩形框被稱為Screen矩形。當中的
內容為用戶可見。將Canvas的信息提供給上層,於是就能夠控制ScreenRect在Canvas上移動。看當中
的pdf。當頁面旋轉,縮放時,Canvas的大小發生變化,這個時候通知上層Canvas發生變化。


還有一類模式是Canvas中僅僅包括有限的頁面。典型的是僅僅有兩個頁面,用來模擬讀書的效果。

這樣Screen
矩形中僅僅能看到Canvas中的頁面,通過點擊頁面,使得Canvas中包括的頁面發生變化,達到切換頁面的
目的。這樣做能夠降低Canvas排版時的開銷。




在后面,僅僅討論第一類模式下的pdf展示。




頁面在Canvas上占領一個矩形。這個矩形稱為Device。在頁面內部。有一個坐標系。稱
之為User坐標系,該坐標系在pdf文件內部使用。一個User坐標系中的點能夠變換到Device矩形中。
當中Device的左上角為原點。

而Device中的點能夠變換到Canvas中,以Canvas的左上角為原點。
相同的,以Screen矩形的左上角為原點。點的坐標又發生變化。更進一步。Screen相對於窗體的位置
知道,還能夠計算出點在窗體中的坐標(一般而言Screen在窗體中鋪滿)。這樣,能夠通過
鼠標位置計算出來pdf內部的元素,進而實現一些功能。




在sumatrapdf中提供了一些基礎的變換工具,通過一個A矩陣。
A = [a b 0;c d 0;e f 1]
來描寫敘述變化。同一時候在高層實現時還提供了Device,Canvas等坐標系之間的高層次抽象的變換工具,其
實現是用較底層的實現的變換,比方:fz_concat,fz_translate。fz_scale等。

。。




2.主要的pdf展示
pdf展示能夠按以下的層次組織api:


最底層應該是"Canvas布局",通過PageInfo數組來表示,PageInfo中記錄了Page在Canvas中的全部信息。




接着一個層次稱為"可見區域":給出了Screen在Canvas中的位置,以及Screen本身的大小,PageInfo中
含有很多其它的信息。比方可見部分的比例(用於計算),頁面在Screen中的位置,第一個可見頁面和最
后一個可見頁面(自己加的用於優化)


在"可見區域"上是"渲染請求"。用於向渲染器請求開始渲染頁面。


再接着就是"導航":上一頁,下一頁,最前一頁,最后一頁,縮放,滾動,旋轉。
"導航"層依賴於其它層次,適當的時候發起渲染請求,窗體重繪請求。


在須要繪制時依據當前繪制信息("可見區域"層計算出來的東西)。從頁面圖像緩存中取出圖像。
然后繪制。通常會先繪制Canvas背景,頁面背景(比方陰影效果,書頁效果),然后再是頁面內容。




這樣,整個展示邏輯比較清楚了(在某些導航下可能一些中間步驟不必要),分為兩條線:


導航->布局->計算可見區域->發起渲染請求->發起重繪請求
接收渲染請求->渲染->緩存渲染結果->按需展示渲染結果


3.渲染器
在底層庫的基礎上,渲染器提供三個不同抽象層次的api:
runPage。renderPage,RenderBitmap
當中runPage是基礎將pdf_page對象展示到fz_device中。能夠控制剪裁矩陣,變換矩陣等。


在mupdf中,有稱為display_list的設備。將page展示到這個設備的時候。會生成一個list,將
該list緩存起來后。能夠通過fz_execute_display_list來加速渲染。


將pdf的內容視為源碼,在解析pdf后形成的一些內部對象視為字節碼,生成display_list時就
相當於把字節碼翻譯為機器碼。


最基本fz_device的莫過於畫圖,當page對象展示到device中的時候就生成相應位圖。利用device
這個抽象,還能夠在展示時提取文字。提取圖片(后面會講)。計算頁面內容占的大小。




renderPage在runPage之上。能夠將page渲染到HDC上。


RenderBitmap會調用renderPage或runPage生成位圖。

覺得在某些情況下使用gdi+有優勢。


另外,還有兩個細節:
一個是頁面分塊,當頁面太大的時候,會控制渲染粒度。
還有一方面在將圖像展示到窗體時,可能出現緩沖未命中。這個時候須要通過返回碼告訴上層。同一時候
還能夠計算出預計的渲染完畢時間,讓上層在完畢時再次Paint。




4.實現文本。圖片選擇
引入一個文本選擇邏輯的類:
第一類選擇方法會給出某個起點和當前點,這樣內部通過計算兩個點所在的glyph,然后把兩個
glyph之間的glyph選中。

選中結果被描寫敘述為頁面和矩形的列表。表示在頁面上有一個矩形是選中的。




第二類選擇方法會給出兩個頁面,然后選中頁面中的全部glyph。


構造器在上述保存一個絕對的選中結果的同一時候,須要提供方法。輸入當前的Screen位置。返回一些
須要在當前Screen上繪制的矩形。


然后還得提供一個推斷方法,表示當前鼠標是否在某個glyph上,以便於上層推斷鼠標是否是在文字上
(這里的glyph都是文字)。

落在的文字能夠是選中的也能夠是未選中的。推斷在選中的文字上用於
右鍵彈出菜單提示復雜文本,推斷在未選中的文字上用於改變鼠標形狀,發起文本選擇的拖動。


此外。還得有個方法取出選中的文本。




文本選擇器是能夠優化的,主要是在全部選中時,這個時候維護的數據結構量大,影響效率。能夠配合
pdf模塊。在全選狀態下。僅僅生成當前可見頁面的選擇數據。當然,在一些用戶行為下須要將全選狀態
清除掉。




圖片選擇同理,僅僅是內部關心的glyph變成了圖片,並且圖片選擇器和文本選擇器須要協同工作(在后
面還會提到文本搜索,這個邏輯也應該和圖片選擇文本選擇協同工作)。




如今還有個問題。怎樣導出pdf中的圖片。


首先依據當前頁面(假設有多個頁面須要知道鼠標位置所在的頁面),拿到一個圖像信息列表。接着
依據當前鼠標位置按一定策略計算出選中的圖像。

於是就得到選中信息了。

另外還要提供獲取圖像數
據的接口。(這里不考慮按住ctrl選中多個圖片。由於永遠在當前頁面操作)。


怎樣獲取圖像信息列表,怎樣獲取當前圖像呢?


前文提到能夠將頁面展示在某個fz_device上,我們能夠新建一個device。
圖像device須要實現fill_image成員,這樣在runPage的時候在遇到圖像會調用fill_image。




device在工作模式為獲取圖像列表的時候,每調用一次則為圖像分配ID,記錄圖像位置。


device在工作模式為獲取圖像數據時,須要知道相應的圖像ID,這樣在展示在該device時每調用
一次還是分配一次ID,直到ID和目標ID相同,這時將數據保存下來。




最后。選擇結果的顯示應該是在OnPaint時,在繪制完當前Screen內容后。再合成上去的。似乎不能
原生地在渲染時也繪制選擇結果。


5.實現文字搜索
一個任務隊列就可以實現。每個任務就是一個搜索請求,任務過程中不斷向主線程發進度消息。
搜索模塊在主線程中收到進度消息時從搜索任務中取結果(注意搜索結果是多線程訪問的)。
在收到進度消息時,假設原有的結果選擇為空。則導航到搜索結果頁面,展示時會顯示這個搜索結果。
同一時候有必要向外通知當前的選中的搜索結果發生變化。

另外還應該向外界通知搜索進度發生變化。




假設有連續的多個搜索請求,僅僅須要把前一個任務停止(搜索任務要能即時停止,僅僅須要在任務中加
一個事件。須要停止時地主線程中激發這個事件),然后再加一個新任務。


6.實現pdf朗讀
首先要求pdf中是有文本的,而朗讀的實現MS有提供:SAPI。

從前面的討論能夠知道,能知道當前頁面
中的文本,於是就能朗讀。假設SAPI能夠回調當前朗讀位置,則能夠實現頁面同步滾動。假設不能
回調當前朗讀位置,也能夠通過每次增加一小段須要朗讀文本的方法,實現按文本段落同步滾動。甚至
玩得花哨一點,還能夠把當前朗讀文本高亮起來。


第三部分 pdf優化


7.1首次展示優化
7.1.1 明白什么時候pdf開始繪制
在展示pdf時有非常多非常多配置項,最好要求上層有一個統一的初始化,在初始化完畢后就能夠開始渲染。


比方。影響pdf展示的有ScreenRect大小。起始頁面,背景圖(顏色),邊距信息等。




要注意兩個點。一個點是什么時候開始渲染,最好是有明白的接口。在接口調用前pdf處於一個初始
化的狀態。依據上層調用來初始化配置。

在接口調用后就開始渲染pdf。

還有一個點是上層不要頻繁變
化配置,否則會導致上次渲染結果失效。

比方上層在通知pdf開始展示后再把歷史記錄中的上次位置
應用到pdf上,比方顯示的窗體(影響ScreenRect)發生變化。


7.1.2主動觸發重繪
在首次渲染完畢后能夠通過自己定義消息強制重繪。不必等上層等到Timer再觸發繪制。


7.1.3 outline載入
pdf_load_outline這函數沒有必要在pdf載入時調用。等須要時再調用。




7.1.4 字體載入
create_system_font_list會掃描一下系統的字體,然后得到某個數據結構。大概會掃描幾百兆文件,
文件數量也非常多。掃描過程中會在文件里跳着讀一些信息。RP好的時候非常快。和系統及磁盤的緩存
機制有關。RP差的時候可能得十幾秒,無法忍受。

所以。這里的數據能夠自己緩存起來。




另外mupdf中還有一些宏。控制着一些內建字體數據,能夠把這些數據丟掉,以降低pdf模塊大小。可是
可能會造成少量的pdf文件亂碼。


7.1.5 圖片背景顏色識別
大圖片渲染慢。我們能夠展示和頁面背景類似的顏色,這樣,在展示時會先顯示背景色,過一會兒
再展示內容,那么這種閃爍較小。怎樣識別背景顏色呢?在頁面上先幾個矩形區域,計算主元素
得到的值能夠覺得就是當前頁面的顏色。而依據已經渲染過的頁面的顏色能夠預測沒有渲染過的頁面
的顏色。而渲染過的頁面顏色能夠記錄在內存中。

當然,不同頁面的背景色不同,或者沒有背景色(
背景有非常多顏色,可是不存在某種顏色占優勢),識別了也沒實用。


7.2 選擇繪制優化
選擇就是給出一堆矩形。然后繪制出矩形的並。

由於不能原生地繪制選擇效果。所以是在pdf渲染完畢后。
后期做AlphaBlend。gdi+能夠依據region來繪制。只是實際效果不太好,或者是有些參數沒有設置正確。


繪制矩形並的問題是。同一塊區域兩次AlphaBlend。和一次AlphaBlend的效果不同。所以必須保證一個區域
僅僅做一次AlphaBlend。

一方案是先把矩形集合繪制到一個圖上,然后兩張圖做AlphaBlend。還有一個方案是先
計算出矩形的並。然后分別單獨繪制這些矩形。計算矩形並不是常簡單,掃描線算法。在y方向上做離散化。然后
在x方向上掃描。


7.3 多渲染模式
回顧前面說的展示pdf的接口的層次,"布局"。"可見區域","渲染請求","導航"。我們從"渲染請求"
這層入手引入多渲染模式。

這里"渲染請求"簡單地說僅僅是渲染一些頁面。渲染器會渲染並緩存起來(有
可能的話頁面分分塊),等待展示時再顯示出來,假設在展示時發現丟失則自己主動發起請求。考慮這樣一
個情形,拖動滾動欄:pdf模塊在收到請求后,依據當前位置發起渲染請求。

然后收到OnPaint消息后進
行繪制,繪制時和收到請求時有一個時間差,在這段時間內,可能收到新的請求,當前的Screen位置已
經發生變化,於是。繪制失敗。僅僅顯示背景色。所以須要引入新的渲染模式。




上面提到的已有的渲染緩存繪制的圖像可能比較多,由於是按頁分塊繪制。假定一個頁就是一塊,那么
在同一個Screen中能看到兩個頁的時候,就須要繪制兩頁。

假設頁面有分塊,比方一個頁面分為4塊。
會使得繪制效率有所提升,額外的渲染少一點。

自然而然,能夠引入一個渲染模式,僅僅繪制當前Screen
中的部分。考慮僅僅繪制Screen的內容的特殊性,我們將渲染請求隊列的限制大小為1,也就是說繪制的內
容永遠是最后一次請求時應該顯示的畫面。盡管有這種渲染結果,可是我們無法決定顯示哪個結果。
所以還須要引入一個狀態控制變量,控制變量會控制在OnPaint時選擇哪個結果緩存中的內容(這個變量並
不控制哪種渲染模式工作,哪種渲染模式不工作,而是負責控制取哪個渲染模式的結果,從后面的分析
能夠看到。兩個渲染模式能夠都工作。並行的)。將兩種渲染請求分別種為"普通渲染請求","Screen渲染
請求"。渲染結果稱為"普通渲染緩存","Screen渲染緩存"。


當拖動的時候。將狀態切換為顯示Screen緩存中的內容。這個時候顯示的最大特點就是和OnPaint時的
Screen的位置無關,顯示是強制的。假設全部是拖動請求,那么顯示的將是拖動過程中遇到的全部畫面的
子集,顯示的頁面越多,說明渲染速度越快,達到了盡可能向用戶呈現結果的目的。假設在老的渲染模式
中主動丟一些幀。可是也不能達到這種效果。顯示時和渲染時的時間差是硬傷,所以引入一個狀態控制
變量,表示顯示的東西和當前的Screen位置無關。




"渲染請求"層的API依據當前的展示模式,發起不同的渲染請求。

在發起Screen渲染請求時,要注意請求的
設計中要能描寫敘述當前頁面的全部狀態。一般包括,顯示的頁面信息。文字選中信息,圖像選中信息,搜索
結果顯示。同一時候在發起Screen渲染請求時。能夠順便再發一個普通的渲染請求,盡量保證在切換到展示"普
通渲染結果"時有結果。不會出現白屏。而在發起普通渲染請求時能夠把當前頁面附近的放在前面,然后順
便放一些當前可見頁面前后的頁面渲染請求。




還須要提供放棄"Screen渲染緩存"的API。由於有的時候須要放棄,見后面分析。


可是引入兩種模式會帶來新的問題:


問題1. 怎樣實現兩種模式的渲染?
兩種渲染能夠在同一個線程。可是問題有兩種請求時怎樣決定先渲染誰,一定是Screen請求嗎?這個難以
回答,所以開個線程中,兩個線程一起干活。

非常不幸。pdf的渲染內核不是線程安全的。於是就在上面加
個鎖吧。渲染本質是單線程的。可是通過系統來決定渲染誰,系統鎖的算法,兩個線程的工作狀態將影響
誰先渲染。當年。就姑且這樣干了,如今回憶起來。還有方案的:多進程渲染。渲染進程分兩種,一種是
傳統的渲染方法。還有一種是按Screen渲染的方法。傳統渲染的能夠開多個進程,有可能的話。在主進程還
有個渲染線程。而Screen渲染的按其定義應該僅僅開一個進程。

這樣,渲染模塊在處理Screen渲染上沒大變化,
而傳統渲染涉及將請求分布到渲染線程,渲染進程上。在顯示時,要知道渲染線程。渲染進程的緩存有哪
些,然后繪制,有可能的話。再次派發請求。

不折騰的話。就留一個渲染線程,開一個Screen渲染進程足
矣。終於策略應該取決於性能分析結果。


於是。實現問題搞定,使用兩個線程假並行。




問題2. 怎樣實現兩種模式的無縫切換?
顯示。Screen渲染不是萬能的,兩種模式各有優點。兩種展示模式之間可能切換。而當中的一類問題是。
從展示Screen渲染緩存切換到展示普通渲染緩存時,普通渲染緩存不命中。
上面已經提到,在"渲染請求"時。在發起"Screen渲染請求"時也順便有普通的渲染請求。這能對問題的
解決起促進作用。

還有一點是能夠在Screen渲染完畢時。發出一個"假Paint消息",收到這個消息時,渲染
器負責把當前的Screen展示到NULL的dc上。其作用是更新緩存。




當然。上面兩個策略能盡量降低Screen渲染展示到普通渲染展示時的白屏現象。不能徹底解決。




從普通渲染緩存展示切換到Screen渲染緩存展示會有什么問題呢?
假設切換到展示Screen渲染緩存時。已經有緩存結果了,在新的Screen沒有渲染出來時,收到OnPaint消息,
於是舊的結果就被展示,呈現出一些古怪的,令人啼笑皆非的現象。

這個問題非常好解決,提供一個放棄
"Screen渲染緩存"的API,僅僅要切換到Screen渲染緩存。則要事先運行一次放棄緩存的邏輯。

當然,有可能
在放棄后,又有新的渲染結果被填進去,這個不用考慮。




在Screen模式不變的情況下也可能出現故障:
比方跳到第5頁。使用展示Screen渲染緩存的模式。然后再跳到第7頁。

這個時候也應該刪除一次Screen渲染
緩存。

當然不是說有的Screen模式不變的情況下都要刪除上一次的緩存,比方前面說的,拖動。拖動就是要
利用上一次渲染的結果,使得拖動時不會太難看。


在引入新的模式后。在拖動時會切換展示模式,可是不是全部的文檔都須要切啊。假設渲染速度非常快,我們
就不切切。

這個非常easy,依據已有的渲染結果預測渲染速度。依據速度來決定展示模式的切換策略。




關於多渲染模式的很多其它思考:這種模式能應用於很多其它的文檔展示。

能在模式中增加第三個模式,可是之間
同步的復雜度會更高。

進一步思考能夠知道。在做一件事的時候能夠多策略結合。相互補充。


7.4 圖像顯示


大圖像顯示是個難題。記得mupdf在讀完圖像流的時候會直接解碼為位圖。

能夠嘗試直接把壓縮的圖像保存起
來。等到終於展示的時候再顯示。

可是整個顯示過程過於復雜,各種變換,因此也僅僅能在顯示前圖像解碼。


整個過程僅僅是把解碼時間推遲了。

這樣做有個優點。在圖像緩存時內存占用少。(另外sumatrapdf應該在
pdf_image.c中載入圖像的函數中,限制緩存圖像的大小。否則展示一些大量圖像構成的pdf時會內存不夠)。


后來也考慮過intel性能元件庫,ffmpeg中某些實現,只是效果都不理想。




猜想,要解決問題須要從整個pdf的渲染框架出發。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM