PDF分页加载 —— PDF.js支持的部分与PDF文档Catalog的调整
PDF能否分页加载,对PDF阅读器来说是一个至关重要的问题。试想一下,假如我们的PDF文件的页数有几百上千页,文件总大小能有几百MB甚至以达到1个GB。如果要等到PDF文件全部加载完毕,才能开始展示,那未免过于可怕。因此,分页加载对我们来说是至关重要的。但是PDF的分页加载实现起来并不简单,并不是说加一两个按钮或者开关,就能够实现了。要若圆满的实现PDF的分页加载,我们需要下一份功夫。对于PDF本身,它也是要满足一些关键条件之后,才能够谈分页加载的。
通过分页加载我们可以按需读取PDF文档的页面
分页加载所需要的前提条件
1.PDF本身的文件结构是否支持分页?
单纯从数据结构的角度来看,PDF本身的文件格式是支持分页的。对于一个PDF而言,除了那些展示给我们看的页面内容之外,还有大量其它相关信息,如目录、索引等信息。这些信息分布在PDF的头部和尾部。通过这些信息可以快速的定位到PDF页面的位置。因此但从PDF本身的结构来看,是可以支持分页的。
2.是否所有的PDF文件都支持分页?
PDF自身的结构决定了它是可以支持分页的。但是这并不意味着所有的PDF文件都支持分页加载。能够支持分页的PDF,自身的目录和索引信息必须要被合理的组织好,这样PDF文件才能够被分页加载。如果一个PDF自身的目录和索引信息乱七八糟的,甚至就是错误的,那么这个PDF是没有办法分页加载的。要想实现这种PDF的分页加载,需要对PDF本身结构内容进行一个重组。
对于一个目录和索引不太有效的PDF,想要知道某一页的信息,需要加载非常多的信息才能保证这个页面能被正常渲染。在实际的分析过程中,我们发现了对于word直接转出来的PDF是可以直接分页加载的,不过有一定的优化空间。对于有些程序转换出来的PDF,是没有办法进行分页加载的。需要我们另写代码去修改该PDF的组织结构。
3.PDF.js支不支持分页加载?
PDF.js能否支持分页加载取决于浏览器,浏览器要能够支持分页读取。Chrome和Firefox毫无疑问是支持的。至于其它的浏览器支撑的情况如何,那要具体情况具体分析了。
总结
想要让一个PDF能够被顺利的分页加载,至少需要满足两点,一是PDF文件本身没问题,是可以被分页读取的,二是浏览器也要支持分页加载。在这两个条件被满足的情况下,我们才可以谈分页加载。这两点目前对我们来说并没有太大问题。浏览器方面可以提示用户使用chrome和firefox能够获取更好的体验。而对于不太符合分页结构的PDF文档,我们编写了代码重新组织了他们的结构,让其具备分页加载的能力。
分页加载的原理与具体的实现过程
默认情况下,PDF阅读器的分页功能是不打开的,需要我们手动打开一下。具体的打开方法就是将disableAutoFetch、disableRange、disableStream这几个参数配置正确。在前面的文章中已经详细的介绍了如何配置这几个参数。
在讨论分页加载的原理之前,我们可以先了解一下文件读取到底是怎么回事。我们先以Java为例来分析文件读取的过程。当我们使用Java读取文件的时候,一般是通过输入流InputStream来读取文件,读取文件的时候不一定需要逐个读下去。我们是可以通过自己的需要跳过一部分的,也是可以通过reset从回到最初的地方的。下面一段代码简单的描述了文件的读取过程。
// 打开文件
File file = new File("test.txt");
// 创建文件流
FileInputStream fis = new FileInputStream(file);
// 读一个字节的数据
fis.read();
// 跳过一定字节
fis.skip();
// 重置文件
fis.reset();
我们可以通过以上的代码——也即JDK提供的一系列API,来实现对文件的读取,而无需关心文件所存放的位置、文件所在的操作系统、文件系统本身的格式、使用的存储介质等。文件是存在机械硬盘、固态硬盘还是在其它什么盘上——甚至文件有可能就单纯的存在内存中,其实我们其实是不关心的。我们关心的只是能够有API来读取文件,来控制文件的偏移。至于数据的实体在那里,在这里并不非常重要,只要它是个二进制的流能被我们读取就可以了。
PDF.js角度来看,道理和逻辑也是一样的。PDF.js一样通过API来读取PDF文档的信息,无论这个文档是存在本地还是远程的网络上,只要文档的提供者能够提供类似于上述的API来让我们读取、跳过、重新定位文件内容,就够了。事实上,PDF.js也确实是这么做的,通过统一API的实现,能够让PDF.js访问远程的、网络上的文件,能够像访问本地的文件一样。当然这也需要后端的支持,当PDF.js向后端发出一个请求,表示自己要(0,100K)这个区间的数据的时候,后端能够争取的处理并返回。这里在强调的并不是速度,而是从开发的角度来看的。PDF.js通过封装底层的请求,将文件的读取工作包装成一个Stream,就像我们上面代码中的FileInputStream一样,然后将这个Stream拿去给PDF的解析者去调用。PDF的解析者只管从这个Stream里获取数据,而不考虑Stream内部究竟是如何将文件拿到手的。下面是一段简单的代码:
class NetworkPdfManager{
...
// 比较抽象的方法,能够帮pdf.js实现调用obj[prop]方法
async ensure(obj, prop, args) {
try {
const value = obj[prop];
// 正常执行
if (typeof value === "function") {
return value.apply(obj, args);
}
return value;
} catch (ex) {
// 只处理数据缺失的异常,其它异常直接抛出去
if (!(ex instanceof MissingDataException)) {
throw ex;
}
// 如果在执行代码的过程中发现缺少数据,需要从服务器读出来
await this.requestRange(ex.begin, ex.end);
// 读出来之后再重新执行这个方法
return this.ensure(obj, prop, args);
}
}
...
}
通过这个方法我们能够清晰的看到PDF.js如何实现将远程数据当做本地数据一样来处理的。通常情况下,PDF.js正常处理PDF,但是它处理到某些数据的时候,发现数据不存在,于是它就抛出一个MissingDataException。这个异常在被捕获之后,立刻开始根据这个异常里面的信息,分析出缺失的数据信息,然后向后台发起请求,将缺失的数据从服务器上读取出来。读取完毕之后,再重新开始处理。
不过既然是分页加载,最终的HTTP请求还是要有代码去调用和执行的。具体实现HTP请求的类有两个,一个是PDFFetchStreamReader,另一个是PDFFetchStreamRangeReader。他们使用fetch方法向远程请求。PDFFetchStreamReader主要是负责请求文件的长度的。而PDFFetchStreamRangeReader则是根据范围来向后台请求数据,它的构造器共有三个,而其中两个分别是begin,end,即需要分步加载的文件范围。
PDFFetchStreamReader读取数据的方式有一点特别。我们调试了好些次,发现打开分页加载功能之后,这个地方特别容易出问题。这个类的最主要的任务是获取到整个PDF文档的长度。但是它是怎么获取的呢?它先向后台发起获取文件的请求,让后台来告知自己文件的长度。而后台需要做的事则是先将整个文件的长度获取到,然后放在请求头的Content-Length字段上传递给前端,然后再将整个文件传递过去。但是前端并不一定会接受这个文件,如果这个文件大小小于2倍的分页大小(即我们前面设置的rangeChunkSize),那就接收。否则PDF.js会将输入流直接终止掉,后台在持续的传输过程中会发现前端已经将连接断开了,也自然而然会抛出Excepiton并终止传输。这是通过一种异常的方式来实现文件读取的,写法也不太具备普适性。如果我们不去修改这个逻辑,我们的文件分页加载产生非常奇怪的问题。
这个逻辑体现在前台的代码上的体现是这样的:
fetch(url,createFetchOptions(...)).then(response => {
// 请求code必须是200或206,206是分段
if (!validateResponseStatus(response.status)) {
throw createResponseStatusError(response.status, url);
}
this._reader = response.body.getReader();
this._headersCapability.resolve();
const getResponseHeader = name => {
return response.headers.get(name);
};
// 这里非常关键,通过获取请求头中的ContentLength来获取文件长度
// rangeChunkSize值也为非常关键,ContentLength小于2*rangeChunkSize就接收内容,否则不接收
const { allowRangeRequests, suggestedLength } =
validateRangeRequestCapabilities({
getResponseHeader,
isHttp: this._stream.isHttp,
rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange,
});
...
this._contentLength = suggestedLength || this._contentLength;
// 决定不接收文件内容
if (!this._isStreamingSupported && this._isRangeSupported) {
this.cancel(new AbortException("Streaming is disabled."));
}
})
为了让我们的分页加载功能能够丝滑的实现,这里我们做了一定程度的优化。我们觉得PDF.js的原来实现有一定的问题,至少在接入后端之后的分页加载,现有代码已经无法满足我们的需求了。因此我们需要对其进行改动。
这个请求在开启分页的情况下,最重要的任务是获取到整个文件的长度。因此我们在修改代码的时候要保证它的整个功能不能发生变化。要获取一个文本的长度其实是一件很简单的事,把文本的长度从后端传到前端也是一件简单的事。但是这里把这个字段放在ContentLength这个字段上,就会引起特别的麻烦。因为ContentLength这个字段是有特殊含义的,它代表整个请求体的长度。因此不管是浏览器还是Nginx之类的代理服务器,都会根据这个字段对请求做一定的处理。
最具代表性的问题是,在某些HTTP请求的程序中,既然已经指定了ContentLength,要么浏览器、服务器就必须要按照这个值获取这么多数据过来才会将这个请求完结,或者等很长时间HTTP请求自动中断。我们需要的东西仅仅是一个文件长度的数据,但是PDF.js却需要我们把这个数据放在一个重要的ContentLength请求头上。我们感觉非常不妥当,而且在实际处理过程中,也出现了很多问题。
我们认为既然PDF.js只是需要一个长度的信息,那么我们就把长度传给它就好了。至于请求体,要么不传,要么少传一点。但是我们不需要用ContentLength这个有特殊含义的HTTP请求头。我们新加了一个请求头——DocumentLength,用这个请求头字段来表示文档长度,至于ContentLength,直接默认0了。当我们向后台请求数据的时候,后台算出来数据直接放在DocumentLength上就可以了,这个字段是我们自定义的。因此不存在太多的问题。于是我们对代码有了下面的改动:
function validateRangeRequestCapabilities({
getResponseHeader,
isHttp,
rangeChunkSize,
disableRange,
}) {
...
const returnValues = {
allowRangeRequests: false,
suggestedLength: undefined,
};
// 原来这里是Content-Length,我们改成了Document-Length
const length = parseInt(getResponseHeader("Document-Length"), 10);
if (!Number.isInteger(length)) {
return returnValues;
}
returnValues.suggestedLength = length;
...
}
经过这次的改动之后,PDF.js不再在需要读取ContentLength的值了,而是通过读取DocumentLength来获取PDF文档的长度。改动完成后,分页加载中读取文档长度的效率大大加快,只需要几毫秒就可以请求完整个文档的长度,也不存在各种因为ContentLength和body长度不一致导致的各种的问题了。同时也省下了大量的带宽、CPU和内存等资源了。
在解决完PDF.js获取PDF文件长度之后,下一步要开始处理分页的问题了。但是这反而是一件相对简单的事了。在PDF.js需要加载某段数据的时候,它就会向后台发出请求。这个请求里面带着一个参数,叫range,range记录了需要加载的分段数据信息。后台在收到这个带有range的请求之后,需要将这个range解析出来。然后按照range的范围,从文件上将数据提取出来,并返回给前端。这样,分页加载的功能就实现了。需要注意的是,返回的HTTP状态应该是为206。
下面展示一段简化后的后端处理分段的代码:
String rangeSource = request.getHeader("range");
String[] range = ... ; // 将rangeSource中的范围提取出来
startByte = Integer.parseInt(range[0]);
endByte = Integer.parseInt(range[1]);
// 206表示部分返回
response.setStatus(206);
// 创建输入流读取文本
InputStream bis = new InputStream(...);
// 跳过一定字节,从start这个地方开始读
bis.skip(startByte);
...
while ((len = bis.read(data, 0, data.length)) != -1 ) {
...
// bos是HTTP请求的输出流
bos.write(buff, 0, length);
// 如果写的足够了就结束
if(finsihWrite){
break;
}
}
让前后端都能够正确的处理PDF的分页请求后,PDF.js这一侧的分页加载功能就算是完全打开了。
尽管如此,我们还遇到了新的问题。那就是当我们按照上面的操作打开和配置好PDF.js之后,是否所有的PDF就可以自由的分页了呢?答案是不可以。因为要想实现对PDF.js的分页加载,不仅需要PDF.js框架的支持,也需要对被解析的PDF支持。并不是所有的PDF文档都能够被分页。而这个问题,则是由于PDF文档本身的Catalog导致的。
PDF文档的Catalog里面包含了对页面目录的组织形式。有的组织形式对分页加载比较良好,有的组织对分页加载不太友好。Catalog的PageTree结构是页面能否分页加载的关键因素之一。PageTree是一个树形结构,它的子节点就是一个一个的Page,即PDF的页面。PageTree有的是单层的,有的是多层的。如果是单层的,即所有的Page都是根节点的子节点,这种情况下,分页加载效果往往不太好。假设我有一个800页的PDF,这个PDF的所有Page都挂在root节点下面,如下面的文本所示:
root
|
---------------------------------------------------------
| | | | ..... | | |
page1 page2 page3 page4 .......... page798 page799 page800
当PDF的页面目录类似于上面的结构的时候,其加载效率是非常低的。因为要加载目录的时候,要把这800页的索引以及可能引用到的其它数据,要全部加载到内存,才能完整的将Catalog构建出来。因此这种形式的数据不太适合分页加载,即使是分页加载的,效率也非常的低下。 但是如果将这个PDF的PageTree重新组织一下,改造成如下面的图所示的模样:
root
|
-------------------------------------
| | |
page001-100 ..... page701-800
| | |
------------------------ ..... -------------------
| | | | | | | |
page1 page2 ... page100 ..... page701 ... page800
通过上图,我们可以看到,虽然page还是800页,但是我们将其分层了。第一层是root,第二层不再是页面节点了,而是一个子PageTree,然后子PageTree下面才是真正的页面。通过改变组织方式,可以让PDF的Catalog少加载很多内容,既不需要加载页面的信息,也不需要加载页面引用的信息。只需要将8个记录有页面信息的子PageTree加载进内存就好了。至于具体的Page信息,等需要用到的时候,再延迟加载,这样效果会非常地好。在实际的运用中我们也发现,几乎大部分PDF都是按照单层结构来记录目录信息的。直接使用office将word转换为PDF,结果就是单层目录。不过office直接转的PDF虽然所有的Page节点都在root节点下,但是其引用的东西并不多。因此效率尚还可以接受。
当然,改造目录本身不会影响页面的数据,并且这个改造代码我们也是可以自己实现的。下面是一段我们实现的、用来改造PDF目录进而加快分页效率的一段Java代码,该代码使用了PDFBox的库来操作PDF。
// 重组页面的结构
public static void regroupPages(COSObject pageTree, List<PDPage> pages) {
int size = pages.size();
int groups = inferGroupSize(size);
// 计算每组的页面个数
int itemsPerGroup = size / groups;
COSDictionary item = (COSDictionary) pageTree.getObject();
COSArray newKids = new COSArray();
COSObject kidObj = null;
COSDictionary kidMap = null;
COSArray kidArr = null;
for (int i = 0; i < size; i++) {
if (i % itemsPerGroup == 0) {
// 按照每一定数量来构成一个PageTree
if (kidObj != null) {
kidMap.setItem(COSName.KIDS, kidArr);
kidMap.setItem(COSName.COUNT, COSInteger.get(itemsPerGroup));
newKids.add(kidObj);
}
kidMap = new COSDictionary();
kidArr = new COSArray();
kidObj = new COSObject(kidMap);
// 类型是PageTree
kidMap.setItem(COSName.TYPE, COSName.PAGES);
// 父节点是root节点
kidMap.setItem(COSName.PARENT, item);
}
PDPage page = pages.get(i);
page.getCOSObject().setItem(COSName.PARENT, kidObj);
}
if (kidArr != null && kidArr.size() != 0) {
kidMap.setItem(COSName.KIDS, kidArr);
kidMap.setItem(COSName.COUNT, COSInteger.get(kidArr.size()));
newKids.add(kidMap);
}
// 用新的结构 替换 旧的结构
item.setItem(COSName.KIDS, newKids);
}
经过这一轮改造,大部分的PDF页面都可以很好的在PDF阅读器里面被很好的加载了。但是仍旧有一些PDF无法很好的分页加载,加载某一页的时候,会出现大量的分段请求。但是实际上该页面的东西并不多。因此我们需要进一步的研究这些PDF。
我们先是调试了PDF.js,研究了PDF.js为什么会发出大量的请求。最后定位到了下面的代码:
async load() {
...
const { keys, dict } = this;
this.refSet = new RefSet();
const nodesToVisit = [];
for (const key of keys) {
const rawValue = dict.getRaw(key);
if (rawValue !== undefined) {
nodesToVisit.push(rawValue);
}
}
// 关键代码,大量遍历数据
return this._walk(nodesToVisit);
}
为了搞清楚PDF在加载一个页面的时候,为什么会大量的向后台请求呢。我们对代码进行了一番详细的分析。在这里,我们发现它们加载了大量的对象。但是这里被大面积加载的对象,大部分都是用不上的,只有一小部分是用得上的。于是我们进一步的研究,想要搞清楚为什么PDF.js会加载这么多种无用的PDF对象。我们先是深入研究了Catalog的内容,发现Catalog里面引用了上千个对象。但其实这些对象的引用是没有必要的,或者说组织的方式并不正确。单个的PDF页面里一般会有一定数量的对象引用,但往往是有限的。
下面的文本是我们从PDF里面截取出来的一部分:
112.0 761.7627 cm BT 0 Ts /FAAABG 11.0 Tf 1.0 0.0 0.0 -1.0 0.0 0.0 Tm 0.0 Tc /GS2 gs 0.0 0.0 0.0 rg Tj ET Q q 1.0 0.0 0.0 1.0 123.0 761.7627 cm BT 0 Ts /FAAABG 11.0 Tf 1.0 0.0 0.0 -1.0 0.0 0.0 Tm 0.0 Tc /GS2 gs 0.0 0.0 0.0 rg Tj ET Q q 1.0 0.0 0.0 1.0 134.0 761.7627 cm BT 0 Ts /FAAABG 11.0 Tf 1.0 0.0 0.0 -1.0 0.0 0.0 Tm 0.0 Tc /GS2 gs 0.0 0.0 0.0 rg Tj ET Q q 1.0 0.0 0.0 1.0 145.0 761.7627 cm BT 0 Ts /FAAABG 11.0 Tf 1.0 0.0 0.0 -1.0 0.0 0.0 Tm 0.0 Tc /GS2 gs 0.0 0.0 0.0 rgTj ET Q q 1.0 0.0 0.0 1.0 156.0 761.7627 cm BT 0 Ts /FAAABG 11.0 Tf 1.0 0.0 0.0 -1.0 0.0 0.0 Tm 0.0 Tc /GS2 gs 0.0 0.0 0.0 rg Tj ET Q q 1.0 0.0 0.0 1.0 167.0 761.7627 cm BT 0 Ts /FAAABG 11.
其中/FAAABG、/GS2就是引用的对象,这些对象的的本体存放在别的地方,在这里出现只是表示这里引用了这个对象。因为我们要引用它,所以必须要记录这个对象原始的信息,如被引用的对象在哪里、被引用的内容是什么之类的。这个信息可以记录在页面(即Page)上,也可以记录在Catalog里面。但是如果记录在Catalog里面的话,Catalog里面的对象信息就会非常的大。值得注意的是,Catalog作为一个PDF的目录和索引,是打开一个PDF首先要读取的文件信息,因此如果Catalog非常大,那么会导致PDF读取单页数据的时候效率很低。试想一下,前面的PDF有800页,如果这800页的对象引用全部存放在Catalog里面,那这样的Catalog将会非常巨大,并且会进一步导致分页加载PDF的时候非常吃力,最终展现出来的分页效果十分糟糕。因此我们要对PDF的对象引用做进一步的改造,才能够实现一个较为良好的PDF分页加载。
首先,对象引用信息不能放在Catalog里面。因为想要实高效的PDF的分页加载,第一件事就是能够让PDF.js快速的构建PDF的目录,因此目录里面不能包含太多太复杂的信息。不然的话会导致Catalog加载缓慢,进而导致拖慢整个PDF的加载。
其次,Page里对象的引用信息,应该只包含哪些当前页面需要用到的对象的引用信息。而不应该包含多余的对对象引用信息。这是什么意思呢?以上面的PDF文本为例,其实它一共就引用了两个对象,分别是/FAAABG、/GS2,按理来说,Page的对象引用表里应该也只有这两个对象。但是实际情况可能是Page的引用表里出现了大量的、无用的引用,即Page记录了杂七杂八的对象引用的基本信息,但是这些信息实际上根本就用不上,但是在解析的过程中还要加载。因此对于Page的对象引用表,我们也需要做一次过滤,将哪些没有用到的对象信息给清除掉,只保留确实用到的对象的引用信息。
为此,我们写了一段Java代码,来达成这个目标:
// 遍历Page页面
Iterator<PDPage> it = pageTree.iterator();
while (it.hasNext()) {
PDPage page = it.next();
COSDictionary dict = page.getCOSObject();
if (dict.getItem(COSName.RESOURCES) == null) {
PDResources resources = page.getResources();
...
COSDictionary resourceDict = resources.getCOSObject();
// 重新排列页面记录的对象信息
Map<String, Object[]> resourceMap = extractResourceToMap(resourceDict);
InputStream stream = page.getContents();
.... // 通过读取stream将page信息组装成content
// 分析Content内容,找出真正用到的引用信息
Set<String> res = scanUsedResource(sb.toString());
COSDictionary newRes = new COSDictionary();
// 重新组装对象引用信息
for (String re : res) {
Object[] objs = resourceMap.get(re);
if(objs == null){
continue;
}
String type = (String) objs[0];
COSObject obj = (COSObject) objs[1];
COSDictionary item = (COSDictionary) newRes.getItem(type);
// 没有就创建
if(item == null){
item = new COSDictionary();
newRes.setItem(COSName.getPDFName(type), item);
}
item.setItem(COSName.getPDFName(re), obj);
}
// 用新构建的对象、有效的引用信息 覆盖旧的对象引用信息
dict.setItem(COSName.RESOURCES, newRes);
}
pages.add(page);
}
通过上面的代码,我们完成了PDF页面中Page引用信息的重组——这一般不会影响到PDF的展示。通过这一轮的对PDF对象引用信息的重组之后,每一个PDF页面都只记录确实用到的对象的引用信息。这样在Page加载的时候,就不会过多的加载那些无用的对象了。在我们对PDF再进行这个操作之后,所有的测试用的PDF都可以有效的被加载了。
总结一下,想要在PDF阅读器当中,实现对PDF的分页加载,并不是一件容易的事。主要要改动的地方涉及到两个方面,一个方面是要调整PDF.js这一边,要让它支持分页加载,并且后台能够按照分段的方式返回数据。另一方面,PDF文档本身要能够支持分页加载,对于不支持分页的PDF,我们通过改造其Catalog的组织方式和对象引用的信息,能够让PDF文档支持分页加载。
跨域加载
PDF.js是支持跨域加载的,因为文件本身和服务不在一个域名下也是很常见的事。但是要让PDF.js支持跨域加载,需要做一些特别的处理。我们看一下PDF.js本身是怎么对待跨域这件事的:
const HOSTED_VIEWER_ORIGINS = [
"null",
"http://mozilla.github.io",
"https://mozilla.github.io",
];
var validateFileURL = function (file) {
try {
const viewerOrigin = new URL(window.location.href).origin || "null";
if (HOSTED_VIEWER_ORIGINS.includes(viewerOrigin)) {
return;
}
const fileOrigin = new URL(file, window.location.href).origin;
// 移除下面的代码不能保证PDF阅读器就能够有效的处理跨域信息
// 想要处理跨域信息,服务器端必须要能够正确的配置CORS信息
if (fileOrigin !== viewerOrigin) {
throw new Error("file origin does not match viewer's");
}
} catch (ex) {
...
}
};
通过上面的代码,我们可以很清晰的看到,mozilla.github.io域名下的文件是可以直接加载的。至于其它的跨域文档加载,则会抛出异常。但是我们可以将抛出异常的代码注释掉。上面的两句注释是PDF.js自带的,我在此做了简单的翻译。PDF.js的开发者告诉我们,要想读取跨域的文件是可以的,但是要能够处理跨域的请求头的信息。
那什么是正确的跨域请求头信息呢?以我们自己开发的后台为例。跨域的时候要放开一些请求头,让这些请求头即使是在跨域的情况下,前端也能够有效的处理:
response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges,Content-Range,Document-Length,Content-Length");
上面的代码就设置了一些请求头,允许这些请求头在跨越情况下,仍然能够被访问。Accept-Ranges和Content-Range,都是为了分页加载而放开的,而Document-Length和Content-Length都是为了让前端有效的获取PDF文件长度而放开的。经过我们的改造之后的PDF.js,实际上此处的Content-Length是可以删除掉的。
修改后的一些问题
当然,我们对PDF.js的修改,让PDF.js原有的功能不再像最开始一样好用,比如前进后退和复制粘贴,不过这个只要后续把这些地方的逻辑补足,就好了。