作为PDF.js Chrome extension 的主要作者,我可以分享一些关于为 Chrome 构建 PDF 查看器扩展程序背后的逻辑的见解。
如何检测PDF文件?
在一个完美的世界中,每个网站都将提供具有标准application/pdf MIME 类型的 PDF 文件。不幸的是,现实世界并不完美,实际上有许多网站使用了错误的 MIME 类型。您将通过选择满足以下任一条件的请求来捕获大多数情况:
- 资源使用
Content-Type: application/pdf 响应标头提供服务。
- 资源使用
Content-Type: application/octet-stream 响应标头提供服务,其 URL 包含“.pdf”(不区分大小写)。
除此之外,您还必须检测用户是要查看 PDF 文件还是下载 PDF 文件。如果您不关心区别,这很容易:如果请求与前面的任何条件匹配,只需拦截请求即可。
否则(这是我采用的方法),您需要检查 Content-Disposition 响应标头是否存在,其值是否以“attachment”开头。
如果您想支持 PDF 下载(例如通过您的 UI),那么您需要添加 Content-Disposition: attachment 响应标头。如果标头已存在,则必须将现有处置类型(例如inline)替换为“附件”。不要费心尝试解析完整的标头值,只需将第一部分剥离到第一个分号,然后将“附件”放在它前面。 (如果你真的想解析头部,阅读RFC 2616 (section 19.5.1)和RFC 6266)。
我应该使用哪些 Chrome(扩展)API 来拦截 PDF 文件?
chrome.webRequest API 可用于拦截和重定向请求。使用以下逻辑,您可以拦截 PDF 并将其重定向到从给定 URL 请求 PDF 文件的自定义查看器。
chrome.webRequest.onHeadersReceived.addListener(function(details) {
if (/* TODO: Detect if it is not a PDF file*/)
return; // Nope, not a PDF file. Ignore this request.
var viewerUrl = chrome.extension.getURL('viewer.html') +
'?file=' + encodeURIComponent(details.url);
return { redirectUrl: viewerUrl };
}, {
urls: ["<all_urls>"],
types: ["main_frame", "sub_frame"]
}, ["responseHeaders", "blocking"]);
(请参阅https://github.com/mozilla/pdf.js/blob/master/extensions/chromium/pdfHandler.js,了解使用此答案顶部描述的逻辑的 PDF 检测的实际实现)
使用上面的代码,你可以截取 http 和 https URL 上的任何 PDF 文件。
如果要从本地文件系统和/或 ftp 查看 PDF 文件,则需要使用 chrome.webRequest.onBeforeRequest 事件而不是 onHeadersReceived。幸运的是,您可以假设如果文件以“.pdf”结尾,那么资源很可能是 PDF 文件。但是,想要使用扩展程序查看本地 PDF 文件的用户必须在扩展程序设置页面上明确允许这样做。
在 Chrome 操作系统上,使用 chrome.fileBrowserHandler API 将您的扩展程序注册为 PDF 查看器 (https://github.com/mozilla/pdf.js/blob/master/extensions/chromium/pdfHandler-vcros.js)。
基于 webRequest API 的方法仅适用于顶级文档和框架中的 PDF,不适用于通过 <object> 和 <embed> 嵌入的 PDF。尽管它们很少见,但我仍然想支持它们,因此我想出了一种非常规的方法来在这些上下文中检测和加载 PDF 查看器。可以在https://github.com/mozilla/pdf.js/pull/4549/files查看实现。这种方法依赖于这样一个事实,即当一个元素被放入文档时,它最终必须被渲染。渲染时,将应用 CSS 样式。当我在 CSS 中为嵌入/对象元素声明动画时,将触发动画事件。这些事件在文档中冒泡。然后我可以为此事件添加一个侦听器,并将对象/嵌入元素的内容替换为加载我的 PDF 查看器的 iframe。
有几种方法可以替换元素或内容,但我使用Shadow DOM 来更改显示的内容而不影响页面中的 DOM。
限制和注意事项
这里描述的方法有一些限制:
-
从服务器请求 PDF 文件至少两次:第一次是获取标题的通常请求,当扩展重定向到 PDF 查看器时该请求被中止。然后另一个请求请求实际数据。
因此,如果一个文件只有效一次,则无法显示 PDF(第一个请求使 URL 无效,第二个请求失败)。
-
此方法仅适用于 GET 请求。没有公共 API 可以直接从 Chrome 扩展 (crbug.com/104058) 中的请求中获取响应正文。
-
让 PDF 为 <object> 和 <embed> 元素工作的方法需要在每个页面上运行一个脚本。我对代码进行了剖析,发现对性能的影响可以忽略不计,但是如果要更改逻辑,仍然需要小心。
(我首先尝试使用Mutation Observers 进行检测,这会使大型文档的页面加载速度降低 3-20%,并在复杂的 DOM 基准测试中导致额外的 1.5 GB 内存使用峰值。
-
检测<object>/<embed>标签的方法可能仍然会导致任何基于NPAPI/PPAPI的PDF插件加载,因为它只替换了<embed>/<object>标签的内容,当它已经被插入并且呈现。 When a tab is inactive, animations are not scheduled, and hence the dispatch of the animation event will significantly be delayed.
后记
PDF.js 是开源的,您可以在https://github.com/mozilla/pdf.js/tree/master/extensions/chromium 查看 Chrome 扩展程序的代码。如果您浏览源代码,您会注意到代码比我在此处解释的要复杂一些。这是因为扩展程序无法在 onHeadersReceived 事件中重定向请求,直到我在几个月前实现它(crbug.com/280464,Chrome 35)。
还有一些逻辑可以让多功能框中的 URL 看起来更好一些。
PDF.js 扩展在不断发展,所以除非你想显着改变 PDF 查看器的 UI,我建议要求用户安装 PDF.js 的官方PDF Viewer in the Chrome Web Store,和/或在@987654337 上打开问题@ 用于合理的功能请求。