Chrome 扩展:如何显示 PDF 文件的自定义 UI?

Chrome extension: How to show custom UI for a PDF file?

我正在尝试编写一个 Google Chrome 扩展来显示 PDF 文件。一旦我检测到浏览器正在重定向到指向 PDF 文件的 URL,我希望它停止加载默认的 PDF 查看器,而是开始显示我的 UI。 UI 将使用 PDF.JS 呈现 PDF 并使用 jQuery-ui 显示其他内容。

问:这个怎么做?阻止原始 PDF 查看器非常重要,因为我不想通过显示文档的两个实例来加倍内存消耗。因此,我应该以某种方式将选项卡导航到我自己的视图。

作为 PDF.js Chrome extension 的主要作者,我可以分享一些关于为 Chrome 构建 PDF 查看器扩展背后的逻辑的见解。

如何检测 PDF 文件?

在完美的世界中,每个网站都将使用标准 application/pdf MIME-type 提供 PDF 文件。不幸的是,现实世界并不完美,实际上有许多网站使用了不正确的 MIME-type。通过选择满足以下任一条件的请求,您将捕获大多数情况:

  • 资源由 Content-Type: application/pdf 响应 header 提供。
  • 资源由 Content-Type: application/octet-stream 响应 header 提供,其 URL 包含“.pdf”(case-insensitive)。

除此之外,您还必须检测用户是否要查看 PDF 文件或下载 PDF 文件。如果你不关心区别,那很简单:如果请求符合前面任何一个条件,就拦截它。
否则(这是我采用的方法),您需要检查 Content-Disposition 响应 header 是否存在以及它的值是否以“attachment”开头。

如果您想支持 PDF 下载(例如通过您的 UI),则需要添加 Content-Disposition: attachment 响应 header。如果 header 已经存在,则必须将现有的处置类型(例如 inline)替换为“附件”。不要费心去解析完整的 header 值,只需将第一部分去掉直到第一个分号,然后在它前面加上“附件”。 (如果你真的想解析 header,请阅读 RFC 2616 (section 19.5.1) and 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和httpsURLs上的任何PDF文件。 如果你想从本地文件系统 and/or ftp 查看 PDF 文件,那么你需要使用 chrome.webRequest.onBeforeRequest event instead of onHeadersReceived。幸运的是,您可以假设如果文件以“.pdf”结尾,那么资源很可能是 PDF 文件。但是,想要使用扩展程序查看本地 PDF 文件的用户必须在扩展程序设置页面明确允许。

在 Chrome OS 上,使用 chrome.fileBrowserHandler API to register your extension as a PDF Viewer (https://github.com/mozilla/pdf.js/blob/master/extensions/chromium/pdfHandler-vcros.js).

基于 webRequest API 的方法仅适用于 top-level 文档和框架中的 PDF,不适用于通过 <object><embed> 嵌入的 PDF。虽然它们很少见,但我仍然想支持它们,所以我想出了一种非常规的方法来检测和加载这些上下文中的 PDF 查看器。可以在 https://github.com/mozilla/pdf.js/pull/4549/files 查看该实现。这种方法依赖于这样一个事实,即当一个元素被放入文档中时,它最终必须被渲染。呈现时,将应用 CSS 样式。当我为 CSS 中的 embed/object 元素声明动画时,将触发动画事件。这些事件在文档中冒出来。然后我可以为此事件添加一个侦听器,并将 object/embed 元素的内容替换为加载我的 PDF 查看器的 iframe。
有几种方法可以替换元素或内容,但我使用 Shadow DOM 来更改显示的内容而不影响页面中的 DOM。

限制和注意事项

这里描述的方法有一些限制:

  • PDF 文件至少从服务器请求了两次:第一次是获取 headers 的常规请求,当扩展程序重定向到 PDF 查看器时,该请求被中止。然后另一个请求请求实际数据。
    因此,如果文件只有一次有效,则无法显示 PDF(第一个请求使 URL 无效,第二个请求失败)。

  • 此方法仅适用于 GET 请求。没有 public API 直接从 Chrome 扩展(crbug.com/104058)中的请求获取响应主体。

  • 让 PDF 为 <object><embed> 元素工作的方法需要每个页面上的 运行 脚本。我分析了代码,发现对性能的影响可以忽略不计,但如果要更改逻辑,还是需要小心。
    (我首先尝试使用 Mutation Observers 进行检测,这使大型文档的页面加载速度降低了 3-20%,并在复杂的 DOM 基准测试中导致额外的 1.5 GB 内存使用峰值)。

  • 检测 <object> / <embed> 标签的方法可能仍会导致任何 NPAPI/PPAPI-based PDF 插件加载,因为它只替换了 <embed> /<object> 标签的内容,当它已经被插入和呈现时。当标签不活动时,没有安排动画,因此,将大幅度延迟动画事件的派遣。

后记

PDF.js 是 open-source,您可以在 https://github.com/mozilla/pdf.js/tree/master/extensions/chromium. If you browse the source, you'll notice that the code is a bit more complex than I explained here. That's because extensions cannot redirect requests at the onHeadersReceived event until I implemented it a few months ago (crbug.com/280464、Chrome 35).

查看 Chrome 扩展的代码

还有一些逻辑可以使多功能框中的 URL 看起来更好一些。

PDF.js 扩展不断发展,所以除非你想显着改变 UI 的 PDF 查看器,否则我建议用户安装 PDF.js 的官方PDF Viewer in the Chrome Web Store, and/or open issues on PDF.js's issue tracker 合理的功能请求。

自定义 PDF 查看器

基本上,要完成您想做的事情,您需要:

  1. 加载 PDF 时插入 URL;
  2. 停止加载 PDF;
  3. 启动您自己的 PDF 查看器并在其中加载 PDF。

如何

  1. 使用 chrome.webRequest API 你可以轻松地监听 Chrome 发出的网络请求,更具体地说,是那些将要加载的请求.pdf 个文件。使用chrome.webRequest.onBeforeRequest事件你可以监听所有以“.pdf”结尾的请求,并得到请求资源的URL。

  2. 创建一个页面,例如 display_pdf.html,您将在其中显示 PDF 并随心所欲地使用它们。

  3. chrome.webRequest.onBeforeRequest 侦听器中,阻止资源加载返回 {redirectUrl: ...} 以重定向到您的 display_pdf.html 页面。

  4. 将 PDF 的 URL 传递到您的页面。这可以通过多种方式完成,但对我来说,最简单的方法是在页面 url 的末尾添加编码的 PDF URL,例如编码的查询字符串,例如 [=21] =].

  5. 在页面内,使用 JavaScript 获取 URL 并使用您想要的任何库处理和呈现 PDF,例如 PDF.js.

密码

按照上述步骤,您的扩展程序将如下所示:

<root>/
    /background.js
    /display_pdf.html
    /display_pdf.js
    /manifest.json

所以,首先,让我们看一下 manifest.json 文件:您需要声明 webRequest 和 [=24= 的权限],所以它应该是这样的:

{
    "manifest_version": 2,

    "name": "PDF Test",
    "version": "0.0.1",

    "background": {
        "scripts": ["/background.js"] 
    },

    "permissions": ["webRequest", "webRequestBlocking", "<all_urls>"],
}

然后,在您的 background.js 中,您将监听 chrome.webRequest.onBeforeRequest 事件并使用 [=118= 更新正在加载 PDF 的选项卡] 的自定义 display_pdv.html 页面,如下所示:

chrome.webRequest.onBeforeRequest.addListener(function(details) {
    var displayURL;

    if (/\.pdf$/i.test(details.url)) { // if the resource is a PDF file ends with ".pdf"
        displayURL = chrome.runtime.getURL('/display_pdf.html') + '?url=' + encodeURIComponent(details.url);

        return {redirectUrl: displayURL};
        // stop the request and proceed to your custom display page
    }   
}, {urls: ['*://*/*.pdf']}, ['blocking']);

最后,在您的 display_pdf.js 文件中,您将从查询字符串中提取 PDF 的 url 并使用它来执行任何您想要的操作:

var PDF_URL = decodeURIComponent(location.href.split('?url=')[1]);
// this will be something like http://www.somesite.com/path/to/example.pdf

alert('The PDF url is: ' + PDF_URL);
// do something with the pdf... like processing it with PDF.js

工作示例

可以找到我上面所说的工作示例 HERE.

文档链接

我建议你看看上面指定的 APIs 的官方文档,你可以通过以下链接找到它们: