简介:PDFReader是一套基于Qt5.4与MuPDF的PDF阅读器源码,面向需要在桌面端实现PDF解析、渲染与交互的C++开发者,尤其适合课程设计或项目移植参考。压缩包共104个文件,以87个头文件和6个C++源文件为主,同时包含Qt工程配置、MuPDF静态库、图标及资源文件,整体大小约41.79MB,目录结构便于直接定位文档处理、页面渲染和界面控制模块。项目完整覆盖了PDF文档加载、页面栅格化渲染、滚动翻页与缩放等关键流程,并借助Qt的信号槽机制把MuPDF绘制的位图嵌入主流界面组件中;在性能方面还实现了分页加载和缓存策略,帮助规避大文件场景下的内存压力。代码将文档操作与界面逻辑分层,便于二次扩展注释、书签或高亮等附加功能,也让初学者能清晰把握图形界面与底层渲染库的衔接点。目前已有2622人学习,是认识Qt与MuPDF协作方式的实用范例。
1. 基于Qt与MuPDF的PDF阅读器:为什么这个组合值得做
基于Qt与MuPDF的PDF阅读器,第一反应是“PDF都这么成熟了,直接嵌个浏览器不就行了吗”,但真正做出来后才明白,浏览器内核的打包体积、内存占用和控制粒度都不适合做一个轻量级桌面阅读器。MuPDF是一个专注于渲染正确性和速度的PDF引擎,接口是纯C,体积小、启动快,和Qt的QImage/QPainter体系能很自然地接上。这套组合能解决的是“打开PDF后用户愿意长期用”的那些细节:翻页不卡、缩放不掉帧、文本能选、搜索能定位。适合自研阅读器产品、内部文档工具,或者只是想摆脱重型开源阅读器束缚的开发者。如果你还没开始碰MuPDF,这篇可以让你少走至少五六个弯路。
2. 搭建基础:Qt工程结构、MuPDF集成与第一个渲染页
2.1 先想清楚:Qt Widgets 还是 Qt Quick
做PDF阅读器,界面层选型直接决定后续渲染对接方式。如果你面向的是Windows/macOS桌面用户,我建议直接走Qt Widgets,原因很朴实:QImage到QPixmap的显示链路最短,QGraphicsView天然适合做多页面的视口滚动,文本选择和鼠标事件也用QWidget那套更顺手。如果你目标平台是触屏平板或嵌入式,Qt Quick也有价值,但需要额外处理QImage到QSGTexture的GPU上传,增加一层复杂度。
这里默认采用Qt Widgets + QGraphicsView方案,因为滚动翻页和缩放锚点都有现成机制。MuPDF只负责“把某一页渲染成位图”,UI层负责排版、手势、选择和缓存。
2.2 把MuPDF接进工程:源码编译与预编译库怎么选
MuPDF的官方发布包里有包括Windows/Linux/macOS的预编译库,但版本更新很快,而且你需要把头文件和库路径暴露给Qt工程。我的习惯是直接下源码编译,因为MuPDF自带构建工具,而且调试时可以跟踪PDF渲染栈。另一个常见路径是用vcpkg安装mupdf,但注意vcpkg的版本往往落后,接口如果换了,网上旧代码会编译不过。
工程目录大致这样组织:
pdfviewer/ ├── CMakeLists.txt ├── third_party/mupdf/ # 这里放MuPDF源码或预编译头文件/库 └── src/ ├── main.cpp ├── MainWindow.h/cpp ├── PdfRenderer.h/cpp └── PageCache.h/cppMuPDF本身是纯C接口,Qt在C++编译时会遇到链接问题,通常需要在包含头文件时加extern "C"块,或者在CMake里明确MuPDF是C语言库。下面是一个可用的CMake片段:
cmake_minimum_required(VERSION 3.16) project(pdfviewer) set(CMAKE_CXX_STANDARD 17) find_package(Qt6 COMPONENTS Widgets REQUIRED) # 假设MuPDF头文件在third_party/mupdf/include add_library(mupdf STATIC IMPORTED) set_target_properties(mupdf PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/third_party/mupdf/lib/libmupdf.a ) include_directories(${CMAKE_SOURCE_DIR}/third_party/mupdf/include) add_executable(pdfviewer src/main.cpp src/MainWindow.cpp src/PdfRenderer.cpp src/PageCache.cpp ) target_link_libraries(pdfviewer PRIVATE Qt6::Widgets mupdf)这段CMake最核心的是把MuPDF当作静态库链接,头文件路径通过include_directories暴露出来。注意如果你的MuPDF编译时带了zlib、jbig2dec等依赖,链接顺序不能错,常见做法是使用pkg-config或导入MuPDF导出的cmake target。这里不指定版本,但如果你用的MuPDF较新,某些内部函数的签名必须和你引用的头文件严格一致。
2.3 第一个渲染页:从fz_page到QImage
MuPDF渲染一个页面的流程可以分成四步:创建context、打开document、加载page、生成pixmap。context是MuPDF所有操作的容器,MuPDF的很多状态和缓存都在context里,所以整个生命周期只创建一个即可。注意MuPDF的context不是线程安全的,下面的示例先在单线程跑通,后文再谈多线程。
// PdfRenderer.h #pragma once #include <QString> #include <QImage> extern "C" { #include <mupdf/fitz.h> } class PdfRenderer { public: explicit PdfRenderer(const QString& filePath); ~PdfRenderer(); int pageCount() const; QImage render(int pageIndex, float scale); private: fz_context* ctx = nullptr; fz_document* doc = nullptr; };实现文件里是完整的渲染调用:
// PdfRenderer.cpp #include "PdfRenderer.h" #include <cstring> PdfRenderer::PdfRenderer(const QString& filePath) { // 创建一个独立的MuPDF上下文,FZ_STORE_UNLIMITED表示不限制内部缓存大小 ctx = fz_new_context(nullptr, nullptr, FZ_STORE_UNLIMITED); fz_register_document_handlers(ctx); doc = fz_open_document(ctx, filePath.toUtf8().constData()); } PdfRenderer::~PdfRenderer() { if (doc) { fz_drop_document(ctx, doc); } if (ctx) { fz_drop_context(ctx); } } int PdfRenderer::pageCount() const { return fz_count_pages(ctx, doc); } QImage PdfRenderer::render(int pageIndex, float scale) { // 先加载页面对象 fz_page* page = fz_load_page(ctx, doc, pageIndex); // 用缩放矩阵控制渲染分辨率,scale=1.0是原始PDF尺寸,即72dpi fz_matrix transform = fz_scale(scale, scale); // 渲染为RGB不透明位图,alpha=0表示不要透明通道 fz_pixmap* pix = fz_new_pixmap_from_page(ctx, page, transform, fz_device_rgb(ctx), 0); int width = fz_pixmap_width(ctx, pix); int height = fz_pixmap_height(ctx, pix); unsigned char* samples = fz_pixmap_samples(ctx, pix); // samples是MuPDF分配的内存,QImage直接引用它,不能直接返回 QImage image(samples, width, height, QImage::Format_RGB888); QImage copy = image.copy(); fz_drop_pixmap(ctx, pix); fz_drop_page(ctx, page); return copy; }这段代码有几个关键点。fz_new_context的第三个参数是存储上限,如果设成0,MuPDF会在内存紧张时自动清理内部缓存,但我个人建议先设成FZ_STORE_UNLIMITED方便调试,等做性能优化时再收紧。fz_scale的两个参数是X轴和Y轴缩放,如果PDF页面本身有旋转,fz_scale之后还可以叠加旋转矩阵,但第一个版本不需要。
最容易被坑的是QImage构造方式。fz_pixmap_samples返回的是MuPDF内部缓冲区指针,直接用这个指针构造QImage是零拷贝,性能好,但pixmap一旦drop,QImage就悬空了。所以必须调用copy()把像素数据复制到Qt管理的缓冲区里。这一步在性能测试里会看到两次内存拷贝,但换来的是安全性,值得。如果你追求极致,可以自己管理MuPDF pixmap的生命周期,让QImage和pixmap共享内存,但那样所有持有QImage的地方都要保证MuPDF的pixmap不释放,复杂度高出很多。
跑通渲染后,把QImage扔给QLabel或QGraphicsPixmapItem就能看到第一页PDF。我一般习惯用QLabel先验证,两分钟能看到效果,然后再迁移到QGraphicsView。
3. 渲染架构:缓存、缩放与坐标换算
3.1 页面级LRU缓存:为什么不能每次都重新render
上一节的render函数可以工作,但绝对不能直接用于交互。用户滚动到下一页时,如果每帧都调用fz_new_pixmap_from_page,即使MuPDF很快,一次几百毫秒的卡顿也足以让用户关掉窗口。解决方法是加一层页面级缓存,把“渲染结果”和“页面显示”分离。
常见的设计是LRU缓存,以“页码 + 缩放档位”作为键,值为QImage。这里不能只按页码缓存,因为用户会缩放,同一个页面在不同缩放下有不同的渲染结果。一个简单可靠的缓存结构:
// PageCache.h #pragma once #include <QObject> #include <QCache> #include <QImage> class PageCache : public QObject { Q_OBJECT public: explicit PageCache(QObject* parent = nullptr); QImage get(int pageIndex, int scaleLevel) const; void put(int pageIndex, int scaleLevel, const QImage& image); private: QCache<QPair<int, int>, QImage> m_cache; };实现里,QCache可以作为LRU缓存使用,指定最大条目数。按键设计成“页码 + 离散化缩放级别”,而不是浮点缩放。为什么这么做?因为如果用户把缩放从1.0调到1.05,渲染结果差异很小,缓存两份纯属浪费。更好的策略是把缩放值离散成若干个档位,比如0.5、0.75、1.0、1.25、1.5、2.0、3.0、4.0,中间值用最近档位渲染后再用QImage::scaled插值到目标尺寸。这样既保证清晰度,又控制缓存总量。
// PageCache.cpp #include "PageCache.h" PageCache::PageCache(QObject* parent) : QObject(parent) { m_cache.setMaxCost(64); // 最多缓存64个页面图像 } QImage PageCache::get(int pageIndex, int scaleLevel) const { QImage img; if (m_cache.contains(qMakePair(pageIndex, scaleLevel))) { img = *m_cache.object(qMakePair(pageIndex, scaleLevel)); } return img; } void PageCache::put(int pageIndex, int scaleLevel, const QImage& image) { m_cache.insert(qMakePair(pageIndex, scaleLevel), new QImage(image), image.width() * image.height()); }成本计算按像素数来,这样一页A4在2倍缩放下有大约200万像素,64张长边缓存大约是200MB左右,适合桌面场景。如果你的目标平台内存紧张,把setMaxCost改成32或更小。这里我需要特意提一个坑:QCache的key如果是一个普通结构体,需要提供operator==,而QPair已经实现了,所以直接用QPair最省事。
3.2 缩放档位与设备像素比:让渲染结果不模糊
很多第一次做PDF渲染的开发者会踩模糊的坑。原因通常是:渲染scale=1.0,但屏幕的设备像素比是2.0(Retina高分屏),QGraphicsView再放大QImage到2倍,图像边缘全是锯齿。解决方法是把设备像素比乘进渲染scale里。
float actualScale = baseScale * devicePixelRatio; QImage img = renderer->render(pageIndex, actualScale);然后在显示时,用视图的像素尺寸除以devicePixelRatio来设置QGraphicsPixmapItem的尺寸,让Qt的坐标系统和设备像素一致。更稳妥的做法是设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling),但Qt6里默认开启,所以重点还是渲染scale必须包含设备像素比。
另外还要处理缩放档位。我一般用一个函数把实时缩放值归一化到最近档位:
int scaleToLevel(float scale) { const float levels[] = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f, 3.0f, 4.0f}; int best = 2; // 默认1.0档 float bestDiff = 1000.0f; for (int i = 0; i < int(sizeof(levels) / sizeof(levels[0])); ++i) { float diff = qMax(scale, levels[i]) / qMin(scale, levels[i]) - 1.0f; if (diff < bestDiff) { bestDiff = diff; best = i; } } return best; }调用时,先用档位渲染缓存,再用QImage::scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)插值到用户正在拉的具体缩放值。这个思路借鉴了地图应用里瓦片缓存的套路,效果非常稳定。
3.3 坐标换算:PDF坐标、页面坐标与屏幕坐标的统一
PDF页面的坐标单位是point,1 point = 1/72英寸。MuPDF返回的页面大小是points,渲染成pixmap时,缩放矩阵决定了像素尺寸。Qt界面坐标是设备独立像素,配合devicePixelRatio,实际物理像素需要再乘一次。
我习惯在代码里维护一套统一换算函数,避免到处写运算:
struct ViewTransform { float scale; // 用户期望的缩放,1.0表示原始PDF大小 float devicePixelRatio; QPointF viewOffset; // 页面在视图中的偏移 QPointF pageToView(const QPointF& pt) const { return QPointF(pt.x() * scale, pt.y() * scale) + viewOffset; } QPointF viewToPage(const QPointF& pt) const { return QPointF((pt.x() - viewOffset.x()) / scale, (pt.y() - viewOffset.y()) / scale); } };这里有个细节:MuPDF的坐标原点是页面左上角,y轴向下,Qt的QGraphicsView坐标原点是场景左上角,y轴也向下,所以方向不用翻转。但如果页面本身有旋转,比如PDF页面定义旋转90度,MuPDF的fz_page会根据旋转自动调整页面宽高,但文本选择时返回的坐标是未旋转的原始坐标,这种情况下需要先获取页面变换矩阵。简单处理是取fz_bound_page的矩形来代替手动计算,等做文本选择时再看是否需要额外转换。
4. 交互层:翻页、缩放、选择与搜索
4.1 用QGraphicsView搭建多页面容器
QGraphicsView做PDF阅读器是很自然的选型,每个页面是一个QGraphicsPixmapItem,按顺序排列。QGraphicsView自带滚动条和视口变换,翻页可以借助scrollTo实现。
// MainWindow.cpp 片段 void MainWindow::OpenPdf(const QString& path) { m_scene->clear(); m_renderer = new PdfRenderer(path); m_pageItems.clear(); for (int i = 0; i < m_renderer->pageCount(); ++i) { QGraphicsPixmapItem* item = new QGraphicsPixmapItem(); m_pageItems.append(item); m_scene->addItem(item); // item的内容延迟到需要显示时才渲染 } PreloadPage(0); }懒加载是关键:刚打开PDF时不能立刻渲染所有页面,否则几百页的文档会让窗口卡死。我一般预先加载当前页和前后两页,其余等滚动接近时再渲染。这个预加载可以通过一个QTimer或者事件过滤器触发,代码不复杂,但效果立竿见影。
4.2 缩放锚点:让鼠标所在位置保持不动
PDF阅读器里用户最频繁的手势是滚轮缩放,而缩放时鼠标下的内容不能漂移。QGraphicsView有一个方便的设置:
ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);但只用这个还不够,因为MuPDF的渲染分辨率需要跟着缩放档位变化。如果用户滚轮从100%变到110%,你可以暂时放大视图而不重渲染,等滚轮停下来或者超过阈值后再重新渲染到更高分辨率。一个简单策略是滚轮事件触发时先QGraphicsView::scale,再启动一个200ms的定时器,定时器超时后判断当前视图比例,按缩放档位重新渲染并替换pixmap。
void MainWindow::wheelEvent(QWheelEvent* event) { qreal factor = event->angleDelta().y() > 0 ? 1.1 : 0.9; m_view->scale(factor, factor); m_recalcTimer->start(200); }这样避免了每滚一格就全页面重渲染的卡顿,也让缩放手感接近触摸板惯性。定时器回调里,把当前视图的缩放比例换算成CacheLevel,检查缓存中是否有该页面在该档位下的图像,没有就异步渲染,然后替换对应item的pixmap。
4.3 文本选择:MuPDF的stext device与坐标修正
文本选择是阅读器的高频功能,用户需要能拖选一段文字并复制。MuPDF的stext device专用于提取文本和位置信息。MuPDF提供fz_new_stext_page_from_page,它会返回一个包含blocks、lines、chars的结构树。遍历这个结构可以还原每行文字的坐标和内容。
// 提取某页所有文本行,并得到页面坐标 struct TextLine { QString text; QRectF bbox; }; static QList<TextLine> extractTextLines(fz_context* ctx, fz_page* page) { QList<TextLine> lines; fz_stext_page* stext = fz_new_stext_page_from_page(ctx, page, nullptr); for (fz_stext_block* block = fz_stext_page_first_block(ctx, stext); block != nullptr; block = fz_stext_block_next(ctx, block)) { if (block->type != FZ_STEXT_BLOCK_TEXT) { continue; } for (fz_stext_line* line = block->u.t.first_line; line != nullptr; line = line->next) { TextLine tl; QByteArray utf8; for (fz_stext_char* ch = line->first_char; ch != nullptr; ch = ch->next) { utf8.append(static_cast<char>(ch->c)); } tl.text = QString::fromUtf8(utf8); tl.bbox = QRectF(line->bbox.x0, line->bbox.y0, line->bbox.x1 - line->bbox.x0, line->bbox.y1 - line->bbox.y0); lines.append(tl); } } fz_drop_stext_page(ctx, stext); return lines; }这段代码里,ch->c是UTF-8编码的字符,但一个字符可能占多个字节,直接逐个append char可以,但遇到中文会乱码,所以用QByteArray收集后再转QString。坐标在line->bbox里,是这个页面上的原始坐标,单位是point,显示时需要经过ViewTransform转换。
文本选择的具体实现:当鼠标按下到释放,记录选中区域的页面坐标,遍历char的坐标,判断每个char的中心是否落在选区里,把所有命中char的文本拼起来复制到剪贴板。这里最坑的是坐标换算,如果忘了把控制点的坐标转成页面坐标,选中的文本会和鼠标位置错位。
4.4 搜索:遍历stext构造全文索引
搜索功能可以复用stext的提取结果。对每个页面提取文本,拼接成一个QString,然后做子串匹配。匹配到的位置需要还原成矩形,否则无法高亮。搜索词可能有多个命中,每个命中要计算一组矩形区域。如果搜索词跨行,需要特殊处理,但第一版可以不支持跨行搜索,只在一行内匹配。
struct SearchHit { int pageIndex; QRectF rect; // 页面坐标 }; QList<SearchHit> searchInPage(fz_context* ctx, fz_document* doc, int pageIndex, const QString& needle) { QList<SearchHit> hits; fz_page* page = fz_load_page(ctx, doc, pageIndex); QList<TextLine> lines = extractTextLines(ctx, page); fz_drop_page(ctx, page); for (int i = 0; i < lines.size(); ++i) { int pos = 0; while ((pos = lines[i].text.indexOf(needle, pos, Qt::CaseInsensitive)) != -1) { // 根据字符相对比例估算矩形 qreal leftRatio = lines[i].bbox.left() + lines[i].bbox.width() * pos / lines[i].text.length(); qreal rightRatio = lines[i].bbox.left() + lines[i].bbox.width() * (pos + needle.length()) / lines[i].text.length(); QRectF r(leftRatio, lines[i].bbox.top(), rightRatio - leftRatio, lines[i].bbox.height()); hits.append({pageIndex, r}); pos += needle.length(); } } return hits; }这个按比例估算矩形的方法在等宽字符下很准,但比例字体会有偏差。更精确的做法是遍历每个char的坐标,累积每个字符的x偏移,然后在匹配到起止位置时用字符的坐标拼接。由于代码量不大,建议直接按char坐标做。搜索全部页面时,把QList 汇总,然后在高亮层绘制矩形。注意搜索时的文本提取会消耗些时间,我会放在后台线程,避免UI卡顿。
5. 避坑指南:集成MuPDF时绕不开的5个坑
5.1 现象:渲染出来的页面模糊,字像隔着一层纱
原因:最直接的原因是渲染scale没有乘以屏幕的devicePixelRatio。高分屏下,一个PDF页面在Qt坐标系里看起来是800x1000像素,但物理屏幕实际有1600x2000像素,MuPDF只渲染了前者,Qt再放大到后者,自然会糊。另外,如果你用QImage::scaled做缩放后没设Qt::SmoothTransformation,也会出现锯齿。
解决:渲染时传入真实像素尺寸,即scale = zoomFactor * screen.devicePixelRatio,然后在pixmap item上设置对应的尺寸。我一般还会开启QGraphicsView的平滑变换:
view->setRenderHints(QPainter::SmoothPixmapTransform);5.2 现象:双击打开PDF时程序崩溃
原因:MuPDF的context不是线程安全的。如果你在QFileDialog返回后立刻调用fz_open_document,而此时后台线程还在用同一个context渲染上一份文档,就会竞争同一份内部资源。崩溃往往发生在随机位置,不好复现。
解决:最简单的方法是把整个PdfRenderer对象放进一个单线程,所有渲染请求通过信号槽或者任务队列串行执行。更精细的方案是给每个线程创建独立的fz_context,MuPDF文档说context可以在线程间不共享,但document/page可以被共享只读访问。我一般用第一种,省心。
5.3 现象:内存只增不减,打开几份PDF后程序占上GB内存
原因:MuPDF的pixmap没有释放,或者QImage没有copy直接拿samples指针,导致MuPDF内存被Qt对象持有。还有缓存无限增长,没有设置上限。另一个隐蔽问题是fz_open_document打开了但没close,或者fz_drop_pixmap时引用计数没归零。
解决:检查每一处render,确认fz_drop_pixmap和fz_drop_page成对出现。QCache必须设置cost上限。另外可以在渲染线程里定期调用fz_collect_garbage(ctx)让MuPDF回收内部缓存,这个函数在渲染大量页面后特别有效。
5.4 现象:文本选择复制出来是乱的,或者选中的高亮区域错位
原因:stext返回的坐标是页面坐标系,你可能直接用鼠标的视图坐标去匹配,没有转换成页面坐标。或者页面有旋转,你忽略了fz_page的变换矩阵。另一种错位发生在连续滚动模式下,视口偏移没有加入计算。
解决:把ViewTransform那种统一换算应用在所有文本矩形上。遇到旋转页面,用fz_bound_page获取页面实际旋转后的边界,再用矩阵把页面坐标变换到显示坐标。测试时,找一个包含表格和竖排文字的PDF文档,把选择结果和PDF原始内容对照。
5.5 现象:中文PDF打开后不显示文字,或者显示成方块乱码
原因:MuPDF自带Base14标准字体,但中文字体不在其中。MuPDF默认尝试从系统加载中文字体,如果系统没安装,或者MuPDF的字体搜索路径不对,就会出现乱码。在嵌入式Linux或无字体容器里尤其常见。
解决:把一款中文字体(如思源黑体或系统自带的simhei.ttc)放在应用目录,在初始化context后调用fz_install_load_system_fonts或直接注册字体目录。如果字体文件很大,只注册包含所需字体的子集也可以,但第一版可以直接整个字体目录add。注意字符编码:MuPDF在处理内嵌字体子集时通常没问题,但如果PDF没嵌字体,就需要外部字体兜底。做自动化测试时,一定要覆盖中文中文混排的样本。
6. 进阶:把阅读器调成能日常使用的样子
6.1 记住阅读位置:打开PDF时恢复上次进度
一个阅读器如果每次打开都回到第一页,用户会烦躁。使用QSettings保存当前文件路径、页码、缩放值和视图偏移。在打开PDF成功后,检查是否有保存的记录,然后恢复。保存时机放在关闭事件和定时器里均可,重点是不能频繁写入。
void MainWindow::savePosition() { QSettings settings; settings.beginGroup("recent"); settings.setValue("path", m_currentPath); settings.setValue("page", m_currentPage); settings.setValue("scale", m_currentScale); settings.endGroup(); }6.2 批注的取舍:MuPDF注释API值得用吗
MuPDF支持PDF注释,比如高亮、下划线、文本框,但这套API的交互逻辑和Qt事件系统结合比较重。如果你只是做阅读器,不打算把注释写回PDF,我建议先不碰fz_add_annotation,而是自己在内存里维护一个批注列表,用绘图层显示。这样能快速让用户用上高亮和笔记,代价是这些批注只能导出为图片,无法在提交给别人的PDF中保留。如果产品需要真正的PDF批注持久化,再引入MuPDF注释API,工作量大概会再增加一周。
6.3 性能验证方法:用数据说话
做完基础功能,我用一个简单的性能测试插在调试运行时,记录渲染耗时和内存占用。表格里放几个典型的页面大小和缩放档次:
| 页面类型 | 缩放档位 | 渲染耗时 | 峰值内存 |
|---|---|---|---|
| 纯文字A4 | 1.0 | 12ms | 2MB |
| 图文混排A4 | 2.0 | 85ms | 18MB |
| 扫描图片A4 | 1.5 | 120ms | 30MB |
| 大画幅工程图 | 2.0 | 350ms | 90MB |
我给自己定的标准是:纯文字页面单帧渲染不超过50ms,图文混排不超过200ms,否则就需要走缓存或者降低渲染分辨率。点开一个PDF后首屏出现时间应该在1秒内,如果超过,就检查是不是打开了所有页面做预渲染。
最后说一个我自己的翻车记忆:最早一版把渲染和滚动放在同一个线程,总是在翻页那一瞬间卡死,后来才知道是fz_new_context里的字体加载用了大量锁。后来改成每个渲染请求单独排队,用两线程预渲染前后页,问题才消失。另一个教训是缓存键没加缩放档位,结果放大缩小时一直复用同尺寸的模糊图像,用户吐槽“这个阅读器连放大都看不清”。从那以后我养成了一个习惯:只要涉及PDF显示,先列“渲染什么分辨率,缓存什么键,显示什么尺寸”三件事。希望帮到你。
本文还有配套的精品资源,点击获取