文章基于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。
todo:pdf的文本查找,文本选择,图片
第三部分 pdf优化
4.1首次展示优化
4.1.1 明确什么时候pdf开始绘制
在展示pdf时有很多很多配置项,最好要求上层有一个统一的初始化,在初始化完成后就可以开始渲染。
比如,影响pdf展示的有ScreenRect大小,起始页面,背景图(颜色),边距信息等。
要注意两个点,一个点是什么时候开始渲染,最好是有明确的接口,在接口调用前pdf处于一个初始
化的状态,根据上层调用来初始化配置。在接口调用后就开始渲染pdf。另一个点是上层不要频繁变
化配置,否则会导致上次渲染结果失效。比如上层在通知pdf开始展示后再把历史记录中的上次位置
应用到pdf上,比如显示的窗口(影响ScreenRect)发生变化。
4.1.2主动触发重绘
在首次渲染完成后可以通过自定义消息强制重绘,不必等上层等到Timer再触发绘制。
4.1.3 outline加载
pdf_load_outline这函数没有必要在pdf加载时调用,等需要时再调用。
4.1.4 字体加载
create_system_font_list会扫描一下系统的字体,然后得到某个数据结构。大概会扫描几百兆文件,
文件数量也很多。扫描过程中会在文件中跳着读一些信息。RP好的时候很快,和系统及磁盘的缓存
机制有关。RP差的时候可能得十几秒,无法忍受。所以,这里的数据可以自己缓存起来。
另外mupdf中还有一些宏,控制着一些内建字体数据,可以把这些数据丢掉,以减少pdf模块大小。但是
可能会造成少量的pdf文件乱码。
4.2 多渲染模式
(未完待续)
pdf阅读器开发