PDF.js插件

前言

今天我们来学习下一款非常有意思的插件PDF.js,正如它的名字一样,它是由Mozilla开源的,用来在Web浏览器上预览PDF文件的一款插件。

我们知道,目前很多浏览器已经支持了PDF的在线预览及下载功能,但是展示的样式各异,并且一部分手机浏览器并不支持预览,在开发WebApp遇到预览PDF的功能,可能需要下载下来借助手机第三方软件打开,显然用户体验不够友好。

而PDF.js恰恰解决了以上问题。

如果说PDF.js的缺点,那大概就是它不支持IE 8 及以下浏览器。(PS:PDF.js使用了HTML 5的相关技术,如canvas,理论上不支持HTML 5的浏览器均不能使用)

项目地址:https://mozilla.github.io/pdf.js/

正文

分析

我们来了解并使用下这款插件。

根据上面地址,我们可以下载PDF.js的预编译版本和源码。

PDF.js提供的预编译版本是可以直接使用的,当然我们也可以使用源码自己编译生成PDF.js插件。

我们来简单说下PDF.js预编译版本的基本构成,截止当前,我使用的是 Stable(v2.0.943) 的预编译版本。

它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── build/
│ ├── pdf.js - display layer
│ ├── pdf.js.map - display layer's source map
│ ├── pdf.worker.js - core layer
│ └── pdf.worker.js.map - core layer's source map
├── web/
│ ├── cmaps/ - character maps (required by core)
│ ├── compressed.tracemonkey-pldi-09.pdf - PDF file for testing purposes
│ ├── debugger.js - helpful debugging features
│ ├── images/ - images for the viewer and annotation icons
│ ├── locale/ - translation files
│ ├── viewer.css - viewer style sheet
│ ├── viewer.html - viewer layout
│ ├── viewer.js - viewer layer
│ └── viewer.js.map - viewer layer's source map
└── LICENSE

主要由build和web包构成,build包里的pdf.worker.js是PDF.js的核心处理包,web包里的viewer.js viewer.html viewer.css 用来在Web页面上展示渲染PDF(边框,工具栏等)。

images文件夹里存放一些工具栏图标等内容,locale文件夹里存放各地区语言包。

debugger.js 是debug相关js,我们在使用时可以开启debug输出某些信息来进行调试。

compressed.tracemonkey-pldi-09.pdf 是一个PDF测试类,当我们没有加载自己定义的PDF时,会默认加载此文件。

使用

集成PDF.js插件

我们在SpringBoot项目下引入PDF.js插件并简单使用。

我们新建SpringBoot项目,引入Web模块,生成项目后,我们只需在项目的static文件夹下引入预编译版本的PDF.js即可。

如图:

PS: 为方便管理,我把PDF.js插件放到了一个pdfViewer文件夹里。

upload successful

我们启动项目,通过浏览器访问viewer.html,即 http://localhost:8080/pdfViewer/web/viewer.html

可以看到成功打开了我们的测试PDF。

upload successful

我们如果想打开自己的PDF应该如何操作呢?

我们可以在上述网络地址上加上file参数指向我们的PDF。 http://localhost:8080/pdfViewer/web/viewer.html?file=….. 的形式。

我们在项目static文件夹中新建pdf包,放入我们的PDF,如下:

upload successful

PDF文件在本项目中,访问有两种方式,网络路径访问和相对路径访问。

最后预览效果如下图:

upload successful

如果PDF文件不在本项目中呢?那一定会出现跨域问题,我们来看一下。

访问 http://localhost:8080/pdfViewer/web/viewer.html?file=http://localhost:8081/pdf/IT%E5%85%A5%E8%81%8C%E6%8C%87%E5%8D%97.pdf 可以看到文件不存在。

upload successful

我们F12查看信息,可以看到PDF.js插件出现如下错误:Uncaught (in promise) Error: file origin does not match viewer's

说明出现了跨域问题。

upload successful

如何解决呢? 毕竟不是所有的PDF文件都会放在项目中的。

这就需要解决跨域问题,通常有两种方法:

我们如果仍使用路径的方式,则需要对PDF.js进行配置,通过刚才报错的信息,我们很容易在viewer.js 里找到如下内容。

upload successful

这段js很好理解,设置允许跨域的路径,我们把我们的服务器网络路径添加到HOSTED_VIEWER_ORIGINS对象里,http://localhost:8080。

PS: 这里要注意,viewer.js.map 文件里的 HOSTED_VIEWER_ORIGINS 对象也要改变(预编译版本),否则你会看到很奇怪的缓存问题。

但这样仍是不行的,我们尝试访问 http://localhost:8080/pdfViewer/web/viewer.html?file=http://localhost:8081/pdf/IT%E5%85%A5%E8%81%8C%E6%8C%87%E5%8D%97.pdf 可以看到跨域问题仍然存在。

upload successful

因为要解决跨域问题,服务器也需要进行设置,我们找到8081服务器,添加跨域设置,主要内容如下(SpringBoot项目):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
String apiAllowOrigins = "http://localhost:8080";
if(StringUtils.isEmpty(apiAllowOrigins)){
corsConfiguration.addAllowedOrigin("*");
}else{
corsConfiguration.setAllowedOrigins(Arrays.asList(apiAllowOrigins.split(",")));
}
corsConfiguration.addAllowedHeader("*");
corsConfiguration.setAllowedMethods(Arrays.asList("HEAD","POST", "OPTIONS","GET"));
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);

//跨域设置
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}

这段代码很好理解,apiAllowOrigins设置成我们请求的服务器地址即可。

这时候我们在访问上面的PDF地址,可以看到PDF被加载出来了。

upload successful

我们也可以使用流的方式来加载远程的PDF文件,当然也需要设置跨域属性。

上面的CorsConfig.java 在8081服务器上保持不变,我们在该项目里新增一个类PDFController,用于解析文件并返回流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class PDFController {
@RequestMapping("/getPDF")
public void getPDF(@RequestParam("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception{
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/pdf;charset=UTF-8");
//PDF文件在服务器上的位置,可以通过程序获取,略,我直接写死了
try(FileInputStream input =new FileInputStream("E:\\WorkSpace\\spring-cloud\\pdf1-service\\src\\main\\resources\\static\\pdf\\"+fileName);
ServletOutputStream out = response.getOutputStream()){
byte buffBytes[] = new byte[1024];
int read = 0;
while ((read = input.read(buffBytes)) != -1) {
out.write(buffBytes, 0, read);
}
out.flush();
}
}
}

我这里传入了fileName属性来获取PDF流。

这样在调用PDF.js 插件时,需要先通过ajax获取PDF流拿到PDF,并提供给PDF.js插件。

我们打开viewer.js 找到如下代码。

upload successful

upload successful

这个方法是webView初始化方法,如果没有传file属性,就加载默认的PDF文件。

我们把 file = 'file' in params ? params.file : _app_options.AppOptions.get('defaultUrl'); 这段代码换成如下:

1
file = 'file' in params ? params.file : DEFAULT_URL;

很好理解,就是不用它的默认值,我们传入一个DEFAULT_URL值。

我们新建helper.js,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var DEFAULT_URL = "";
var PDFData = "";
$.ajax({
type:"post",
async:false,
mimeType: 'text/plain; charset=x-user-defined',
url:"http://localhost:8081/getPDF",
data:{fileName:"IT入职指南.pdf"},
success:function(data){
PDFData = data;
}
});
var rawLength = PDFData.length;
//转换成pdf.js能直接解析的Uint8Array类型
var array = new Uint8Array(new ArrayBuffer(rawLength));
for(i = 0; i < rawLength; i++) {
array[i] = PDFData.charCodeAt(i) & 0xff;
}
DEFAULT_URL = array;

并在PDF.js插件的web包下引入helper.js 和 jquery.js。

PS: jquery.js 的引用就使用了一个ajax,如果由其他的ajax方案可以不引入jquery。

upload successful

并在viewer.html里引入依赖,需要在viewer.js 之前引入。

1
2
3
4
5
6
7
8
.....部分代码略
<script src="../build/pdf.js"></script>
<script src="jquery-3.3.1.min.js"></script>
<script src="helper.js"></script>
<script src="viewer.js"></script>

</head>
.......部分代码略

设置好后,我们重新启动下服务器,可以看到PDF文件被加载了。

upload successful

PS:根据上面,我们可以看到当不传入file参数时,PDF.js会加载默认配置,我们可以通过js等控制这个默认配置,让PDF.js每次打开的都是 http://localhost:8080/pdfViewer/web/viewer.html ,但是PDF文件变化,以实现不显示file参数的需求。

PDF.js 插件的个性化需求

我们使用了PDF.js 插件后,可以看到功能很全,什么工具栏,放大缩小,打印,下载等等功能一应俱全,但现实中我们有可能是不需要这么多的。

尤其在WebApp中,我们可能仅仅可以查看PDF即可,偶尔可以有个放大缩小功能,这样应该如何处理呢。

这项功能的实现是非常简单的,我们找到该功能的按钮的button,直接添加hidden属性即可。

upload successful

1
2
3
4
5
6
7
8
9
10
11
12
13
......
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11" data-l10n-id="toggle_sidebar" hidden>
<span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span>
</button>
<div class="toolbarButtonSpacer"></div>
<button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12" data-l10n-id="findbar" hidden>
<span data-l10n-id="findbar_label">Find</span>
</button>
......
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools" hidden>
<span data-l10n-id="tools_label">Tools</span>
</button>
......

PDF.js 踩坑记

  1. 关于过大PDF无法显示的问题

    曾经遇到过测试环境PDF文件预览正常但是生产环境部分过大PDF文件无法打开的问题,后查看请求发现GET请求的range范围为0-65535,判断可能是服务器对 Range 进行了某些特殊限制。

    而PDF.js 插件里有一项参数是可以禁止使用Range的,在viewer.js defaultOptions 对象里,如下:

    1
    2
    3
    4
    5
    6
    var defaultOptions = {
    disableRange: {
    value: false,
    kind: OptionKind.API
    }
    }

    我们将属性设置为true得以解决问题。(如果不生效,可能是map文件存在的缘故,需要重新生成viewer.js.map文件)

  2. PDF无法正常显示

    如果相对路径无法加载,可尝试网络路径。

    如果使用的是网络路径扔无法加载,可以对网络路径进行encode编码在返回给前端调用。

    如果仍不可以,可以考虑使用流传输的方式。

  3. 关于PDF.js 插件的缓存问题

    使用PDF.js插件过程中,你会发现它会缓存看过的PDF的阅读位置,这本是一项人性化的设定,但如果你就是想每次打开PDF文件后从头开始看起,请使用 disableHistory 参数。

    它也在defaultOptions对象里,默认false,改为true后每次再打开这个PDF文件时就会从第一页看起。

  4. viewer.js defaultOptions 对象

    可以看到PDF.js 插件 defaultOptions 对象里还有很多很多的默认参数,我们都是可以对其进行设置以实现相关功能或者禁用相关功能的。

    关于它们的用法,可以参考一些相关文档,这儿就不做过多叙述了。

结语

今天主要介绍了PDF.js 插件的使用,这是一款非常优秀的插件,在WebApp 预览PDF文件时经常会被使用,而且 Mozilla 以后有意将该插件集成到 FireFox 浏览器里,并且通过该插件立志于打造一项Web浏览PDF文件的标准。

让我们拭目以待吧。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道