- 更新:2020-11-16 20:05:14
- 首发:2020-11-12 23:49:23
- 教程
- 4694
对于内存较小的嵌入式设备或者移动设备(例如带屏智能音响,安卓、IOS平台的设备),在线预览PDF功能经常会导致内存溢出、应用程序闪退。
对于非常大的PDF文档,一次性下载整个文档将消耗大量的时间,即使开启分段下载,PDF文档正常下载完成并加载,超大的内存占用也会导致设备运行卡顿。
关于《如何实现高性能的在线 PDF 预览》,原作者(子木)已经描述得非常详细了,这是一篇深度好文,值得认真拜读。我所采用的方案和该文章描述完全一致,甚至前端demo完全采用了这篇文章介绍的方法。对于其中实现原理,我不再赘述,只对方案进行补充说明。
方案目的
提供在线查看PDF文件的JS接口。查看文件时,用户可以对PDF文件的进行旋转、缩放、跳转到指定页码。
在客户端,受限于内存,如果直接预览超大PDF可能内存溢出,即便进行前端分页加载,在首屏加载时也会异常的慢。PDF预览接口需要将PDF的内容交由中间服务进行分片,将一个大的PDF文件分成多个小文件。比如某个PDF有100页,我们按照5页一片,将它切分成20片,每次只下载用户看到的那1-2个分片。然后在用户进行滚动翻页的时候,异步去下载包含对应页码的分片。
PDF文件可能会很大,比如一个1000页的PDF文件。随着用户的滚动浏览,浏览器会一直渲染页码,如果最终同时将所有页面的DOM都加载到页面上,内存占用将会非常多,导致系统卡顿。因此,为了减少内存占用,需要自动清理已渲染但不再需要的内容。
后端主要代码及Demo
后端采用itext7
对PDF进行裁剪。代码很简单,就是照抄官网示例。根据实际需求选用splitByPageCount
(切割为固定页数的PDF文件)或splitByPageNumbers
(切割指定页码),参考https://api.itextpdf.com/iText7/java/7.1.7/com/itextpdf/kernel/utils/PdfSplitter.html#splitByPageNumbers-java.util.List-。
private static String splitPDF(String perpage, String url) throws IOException {
String hash = CryptoUtils.hashKeyForDisk(perpage + "-" + url); // 根据perpage和url获取hash值
String pageCountFilePath = PUBLIC_PATH + "/" + hash + "/pageCount.txt";
String filePath = PUBLIC_PATH + "/" + hash + "/" + hash + ".pdf";
List<String> urlList = new ArrayList<>();
File file = new File(PUBLIC_PATH + "/" + hash);
if (!file.exists() && !file.isDirectory()) { // 如果文件不存在
if (!file.mkdir()) {
return "";
}
FilesUtils.saveUrlAs(url, filePath); // 从URL下载文件
final int maxPageCount = Integer.parseInt(perpage); // create a new PDF per X pages from the original file
PdfDocument pdfDocument = new PdfDocument(new PdfReader(new File(filePath)));
PdfSplitter pdfSplitter = new PdfSplitter(pdfDocument) { // PDF分割
int partNumber = 1;
@Override
protected PdfWriter getNextPdfWriter(PageRange documentPageRange) {
try {
String filePath = PUBLIC_PATH + "/" + hash + "/" + partNumber++ + ".pdf";
urlList.add(filePath.replace(PUBLIC_PATH, HOST));
return new PdfWriter(filePath);
} catch (final FileNotFoundException ignored) {
throw new RuntimeException();
}
}
};
pdfSplitter.splitByPageCount(maxPageCount, (pdfDoc, pageRange) -> pdfDoc.close()); // 根据perpage分割
// pdfSplitter.splitByPageNumbers(maxPageCount); // 根据pageNumber分割
int pageCount = pdfDocument.getNumberOfPages(); // 总页数
pdfDocument.close();
File pageCountFile = new File(pageCountFilePath);
if (!pageCountFile.exists() && !pageCountFile.createNewFile()) {
throw new RuntimeException("Can't createNewFile 'pageCount.txt'.");
}
FileWriter fw = new FileWriter(pageCountFilePath);
fw.write(String.valueOf(pageCount));
fw.close();
Map<String, Object> out = new HashMap<>();
out.put("totalPage", pageCount);
out.put("urlList", urlList);
return JSON.toJSONString(out);
} else { // 如果文件已经存在
int totalPage = 0;
File pageCountFile = new File(pageCountFilePath);
if (pageCountFile.exists()) { // 读取总页数记录
InputStream is = new FileInputStream(pageCountFilePath);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = reader.readLine();
totalPage = Integer.parseInt(line);
is.close();
reader.close();
}
File[] fileArray = file.listFiles();
String fileFirstPath = file.toPath().toString().replace(PUBLIC_PATH, HOST) + "/";
if (fileArray != null) {
for (File value : fileArray) { // 还原下载地址
if (value.isFile() && !value.getName().equals("pageCount.txt") && !value.getName().equals(hash + ".pdf")) {
urlList.add(fileFirstPath + value.getName());
}
}
}
urlList.sort(Comparator.comparingInt(str -> Integer.parseInt(str.replace(fileFirstPath, "").replace(".pdf", ""))));
Map<String, Object> out = new HashMap<>();
out.put("totalPage", totalPage);
out.put("urlList", urlList);
return JSON.toJSONString(out);
}
}
完整的Demo代码:https://github.com/yi-ge/pdf-split
跟子木同学实现方案不一样的是,我一次性返回了所有分片的下载地址。
请求参数:
POST https://pdf.ykfz.pw/
Body(Json):
{
"perpage": 5,
"url": "https://cdn.wyr.me/files/2020-11-12/demo.pdf"
}
返回值:
{
"totalPage": 177,
"urlList": [
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/1.pdf",
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/2.pdf",
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/3.pdf",
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/4.pdf",
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/5.pdf",
"https://pdf.ykfz.pw/91c8c56d2b56e071ecabeddb8d09cbf1/6.pdf",
......
]
}
Demo特点:
- 通过
url
和perpage
计算哈希值,无需依赖数据库,重复请求不会再次下载文件 - 自动清理n天以前的文件
注意:
- 这个Demo真的只是一个Demo,速成的,仅供学习,请勿直接用于生产环境
- 没有进行分布式设计,不支持部署多个Pod运行
- 将
itext7
用于生产环境您可能需要向itext7
支付授权费用 - 支持付费定制,提供
Node.js
版本
前端Demo
线上Demo地址:https://pdf-demo.ykfz.pw/
完整的Demo代码:https://github.com/yi-ge/pdf-split-demo
Demo基本上照搬了子木同学的代码。感谢!
更多
Q: 为什么不将PDF文件转换为图片进行加载?
A: 在Web平台,此方案和直接预览PDF本质上没有太大的区别,也可以做到高性能。itext7
也为PDF转图片功能提供了API。需要根据具体业务场景选择方案,有些PDF带特殊字体,需要针对性做处理。
Q: 为什么说这个Demo不能直接用于生产环境?
A: PDF预览需要考虑的因素较多,例如同一个PDF文件,不同页面的页面大小可能不一样。在高并发环境下,这样需要做大量计算和硬盘IO的任务,是需要进行任务队列限流、操作分布式文件系统或操作对象存储来实现PDF切割的。针对加密PDF文档,需要进行解密;针对有水印的PDF文档,需要还原水印(推荐文章《移动端pdf预览-水印&电子签章问题》);针对有数字签名的PDF,需要进行校检。Demo中的代码都“比较粗糙”,需要进一步完善。
👍 👍 👍
hi,轶哥。我想问下PDF.JS的按需加载是不是要基于PDF分片呢?
不是的哈。自带的按需加载只需要静态资源服务器支持即可。
不是的哈。自带的按需加载只需要静态资源服务器支持即可。
我的意思是那个只显示10页的那种优化方案。我看了看代码还是要获取分片。
嗯嗯,那就用分片的方案呗。
老师你好,我希望能用一个openwrt路由器实现IPv4和IPv6的桥接,请问我该如何实现?我尝试了直接新增dhcpv6的接口,但是效果不甚理想(无法成功获取公网的ipv6,但是直连上级路由的其他设备是可以获取公网的ipv6地)
![%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE20241205230845.png](https://cdn.wyr.me/visitor-files/2024-12-05/1733411344287屏幕截图 2024-12-05 230845.png)你好
,为什么我这里是0039 813C 0600 0075 16xx xx xx,只有前6组是相同的,博客中要前8位相同,这个不同能不能照着修改呢?我系统版本是Win1124H2
大神你好,win11专业版24h2最新版26100.2033,文件如何修改?谢谢
win11专业版24h2最新版26100.2033,Windows Feature Experience Pack 1000.26100.23.0。C:\Windows\System32\termsrv.dll系统自带的这个文件,39 81 3C 06 00 00 0F 85 XX XX XX XX 替换为 B8 00 01 00 00 89 81 38 06 00 00 90。仍然无法远程连接。原来是win11 21h2系统,是可以远程链接的。共享1个主机,2个显示器,2套键鼠,各自独立操作 各自不同的账号,不同的桌面环境。
博主,win11专业版24h2最新版,C:\Windows\System32\termsrv.dll系统自带的这个文件,找不到应该修改哪个字段。我的微信:一三五73二五九五00,谢谢