zoukankan      html  css  js  c++  java
  • 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的渲染框架出发。

  • 相关阅读:
    《柯尔特思维教程》-第3章(交互)- 第3节:证据的价值
    《柯尔特思维教程》-第3章(交互)- 第2节:证据类型
    《柯尔特思维教程》-第3章(交互)- 第1节:CoRT交互 EBS(Examine Both Sides,研究双方的观点)
    《柯尔特思维教程》-第2章(组织)- 第10节:结论
    《柯尔特思维教程》-第2章(组织)- 第9节:巩固
    《柯尔特思维教程》-第2章(组织)- 第8节:集中
    java 创建对象的几种方式
    关于final
    关于静态-static
    重温js浅拷贝、深拷贝
  • 原文地址:https://www.cnblogs.com/mqxnongmin/p/10734936.html
Copyright © 2011-2022 走看看