惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
Project Zero
Project Zero
K
Kaspersky official blog
G
Google Developers Blog
T
Threat Research - Cisco Blogs
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
Y
Y Combinator Blog
Recorded Future
Recorded Future
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
Microsoft Security Blog
Microsoft Security Blog
H
Help Net Security
S
Schneier on Security
P
Palo Alto Networks Blog
H
Hacker News: Front Page
N
News and Events Feed by Topic
N
Netflix TechBlog - Medium
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
SecWiki News
SecWiki News
Cloudbric
Cloudbric
TaoSecurity Blog
TaoSecurity Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Hacker News
The Hacker News
C
Check Point Blog
L
LangChain Blog
腾讯CDC
小众软件
小众软件
T
Tenable Blog
Google DeepMind News
Google DeepMind News
GbyAI
GbyAI
L
LINUX DO - 最新话题
A
About on SuperTechFans
Google Online Security Blog
Google Online Security Blog
C
Cisco Blogs
Recent Announcements
Recent Announcements
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
雷峰网
雷峰网
美团技术团队
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Full Disclosure
博客园_首页

轶哥博客

blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog blog
blog
2020-11-12 · via 轶哥博客

desktop.jpeg

对于内存较小的嵌入式设备或者移动设备(例如带屏智能音响,安卓、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特点:

  • 通过urlperpage计算哈希值,无需依赖数据库,重复请求不会再次下载文件
  • 自动清理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中的代码都“比较粗糙”,需要进一步完善。