如何创建一个JavaScript PDF浏览器

181 阅读7分钟

便携文档格式,简称PDF,是分享包含大量精确格式化的文本和图像的文件的理想选择,特别是当它们可能被打印或离线阅读时。虽然大多数现代浏览器可以显示PDF文件,但它们使用的是一个在独立标签或窗口中运行的PDF浏览器,迫使用户离开你的网站。

PDF.js是一个开源的JavaScript库,它允许你在你的网页中直接解析和渲染PDF文件。在本教程中,我将向你展示如何使用它,从头开始创建一个成熟的自定义PDF浏览器。

如果你要在你的网站上添加一个PDF浏览器,你可能也会对一个专业的翻页书插件感兴趣。JavaScript翻转书以数字翻转书的形式展示你的内容,使用翻页效果,缩放,并支持多种内容类型。

1.创建一个用户界面

让我们从创建一个新的网页开始,并在其中添加通常的HTML5模板代码。

<!doctype html>
 
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My PDF Viewer</title>
</head>
<body>
     
</body>
</html>

接下来,在<body> ,创建一个<div> ,这个元素可以作为我们的PDF浏览器的容器。

<div id="my_pdf_viewer">
     
</div>

在我们的JavaScript PDF浏览器的核心将是一个HTML5<canvas> 元素。我们将在它里面渲染我们的PDF文件的页面。因此,在<div> 元素内添加以下代码。

<div id="canvas_container">
    <canvas id="pdf_renderer"></canvas>
</div>

为了保持简单,我们将在任何时候在画布中只渲染一个页面。然而,我们将允许用户通过按下一个按钮切换到上一页或下一页。此外,为了显示当前的页码,并允许用户跳到他们想要的任何页面,我们的界面将有一个输入字段。

<div id="navigation_controls">
    <button id="go_previous">Previous</button>
    <input id="current_page" value="1" type="number"/>
    <button id="go_next">Next</button>
</div>

为了支持缩放操作,在界面上再添加两个按钮:一个用于放大,一个用于缩小。

<div id="zoom_controls">  
    <button id="zoom_in">+</button>
    <button id="zoom_out">-</button>
</div>

在本节结束时,网页的代码看起来是这样的:

<!doctype html> 
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My PDF Viewer</title>
</head>
<body>
     <div id="my_pdf_viewer">
        <div id="canvas_container">
            <canvas id="pdf_renderer"></canvas>
        </div>

        <div id="navigation_controls">
            <button id="go_previous">Previous</button>
            <input id="current_page" value="1" type="number"/>
            <button id="go_next">Next</button>
        </div>

        <div id="zoom_controls">  
            <button id="zoom_in">+</button>
            <button id="zoom_out">-</button>
        </div>
    </div>
</body>
</html>

2.获取PDF.js

现在,我们的JavaScript PDF阅读器的基本用户界面已经准备好了,让我们把PDF.js添加到我们的网页中。因为该库的最新版本在CDNJS上可用,我们只需在网页末尾添加以下几行就可以做到。

<script
    src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.0.943/pdf.min.js">
</script>

3.加载一个PDF文件

在我们开始加载PDF文件之前,让我们创建一个简单的JavaScript对象来存储我们的PDF浏览器的状态。在它里面,我们将有三个项目:对PDF文件本身的引用,当前的页面索引,以及当前的缩放级别。

<script>
    var myState = {
        pdf: null,
        currentPage: 1,
        zoom: 1
    }
 
    // more code here
</script>

在这一点上,我们可以通过调用pdfjsLib 对象的getDocument() 方法来加载我们的PDF文件,该方法以异步方式运行。

pdfjsLib.getDocument('./my_document.pdf').then((pdf) => {
 
    // more code here
 
});

请注意,getDocument() 方法在内部使用一个XMLHttpRequest 对象来加载 PDF 文件。这意味着该文件必须存在于你自己的网络服务器或允许跨源请求的服务器上。

一旦PDF文件被成功加载,我们就可以更新我们状态对象的pdf 属性。

myState.pdf = pdf;

最后,添加对一个名为render() 的函数的调用,这样我们的PDF阅读器就会自动渲染PDF文件的第一页。我们将在下一步中定义该函数。

render();

4.渲染一个页面

通过调用pdf 对象的getPage() 方法,并向其传递一个页码,我们可以得到PDF文件中任何一页的引用。现在,让我们把我们的状态对象的currentPage 属性传给它。这个方法也会返回一个承诺,所以我们需要一个回调函数来处理其结果。

因此,创建一个名为render() 的新函数,包含以下代码。

function render() {
    myState.pdf.getPage(myState.currentPage).then((page) => {
 
        // more code here
 
    });
}

要实际渲染一个页面,我们必须调用回调函数内可用的page 对象的render() 方法。作为参数,该方法希望得到我们画布的2D上下文和一个PageViewport 对象,我们可以通过调用getViewport() 方法得到该对象。因为getViewport() 方法希望将所需的缩放级别作为一个参数,所以我们必须将我们的状态对象的zoom 属性传递给它。

var canvas = document.getElementById("pdf_renderer");
var ctx = canvas.getContext('2d');
 
var viewport = page.getViewport(myState.zoom);

视口的尺寸取决于页面的原始尺寸和缩放级别。为了确保整个视口被渲染在我们的画布上,我们现在必须改变我们的画布的尺寸,以匹配视口的尺寸。方法是这样的。

canvas.width = viewport.width;
canvas.height = viewport.height;

在这一点上,我们可以继续渲染页面。

page.render({
    canvasContext: ctx,
    viewport: viewport
});

把这一切放在一起,整个源代码看起来像这样:

<!doctype html>
 
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My PDF Viewer</title>
  <script
    src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.0.943/pdf.min.js">
  </script>
</head>
<body>
     <div id="my_pdf_viewer">
        <div id="canvas_container">
            <canvas id="pdf_renderer"></canvas>
        </div>

        <div id="navigation_controls">
            <button id="go_previous">Previous</button>
            <input id="current_page" value="1" type="number"/>
            <button id="go_next">Next</button>
        </div>

        <div id="zoom_controls">  
            <button id="zoom_in">+</button>
            <button id="zoom_out">-</button>
        </div>
    </div>

    <script>
        var myState = {
            pdf: null,
            currentPage: 1,
            zoom: 1
        }
     
        pdfjsLib.getDocument('./my_document.pdf').then((pdf) => {
     
            myState.pdf = pdf;
            render();

        });

        function render() {
            myState.pdf.getPage(myState.currentPage).then((page) => {
         
                var canvas = document.getElementById("pdf_renderer");
                var ctx = canvas.getContext('2d');
     
                var viewport = page.getViewport(myState.zoom);

                canvas.width = viewport.width;
                canvas.height = viewport.height;
         
                page.render({
                    canvasContext: ctx,
                    viewport: viewport
                });
            });
        }
    </script>
</body>
</html>

如果你尝试在浏览器中打开网页,你现在应该能看到PDF文件的第一页。

Demo

你可能已经注意到,我们的PDF浏览器的大小目前取决于被渲染的页面的大小和缩放级别。这并不理想,因为我们不希望在用户与PDF查看器互动时,我们的网页布局受到影响。

为了解决这个问题,我们需要做的是给封装我们的画布的<div> 元素一个固定的宽度和高度,并将其overflow CSS属性设置为auto 。这个属性在必要时可以给<div> 元素添加滚动条,允许用户在水平和垂直方向上滚动。

在网页的<head> 标签内添加以下代码。

<style>
    #canvas_container {
        width: 800px;
        height: 450px;
        overflow: auto;
    }
</style>

当然,你可以自由改变宽度和高度,甚至使用媒体查询,使<div> 元素符合你的要求。

另外,你可以选择加入以下CSS规则,使<div> 元素看起来更加鲜明。

#canvas_container {
    background: #333;
    text-align: center;
    border: solid 3px;
}

如果你现在刷新网页,你应该在屏幕上看到类似这样的东西。

Demo With Background 5.改变当前页面

我们的JavaScript PDF阅读器目前只能显示给它的任何PDF文件的第一页。为了让用户改变正在呈现的页面,我们现在必须给我们先前创建的go_previousgo_next 按钮添加点击事件监听器。

go_previous 按钮的事件监听器中,我们必须递减我们状态对象的currentPage 属性,确保它不低于1 。这样做之后,我们可以简单地再次调用render() 函数来渲染新的页面。

此外,我们必须更新current_page 文本字段的值,使其显示新的页码。下面的代码告诉你如何做。

document.getElementById('go_previous')
    .addEventListener('click', (e) => {
    if(myState.pdf == null|| myState.currentPage == 1) 
    return;
       
    myState.currentPage -= 1;
    document.getElementById("current_page").value = myState.currentPage;
    render();
});

同样,在go_next 按钮的事件监听器内,我们必须增加currentPage 属性,同时确保它不超过PDF文件中存在的页数,我们可以使用_pdfInfo 对象的numPages 属性来确定。

document.getElementById('go_next')
    .addEventListener('click', (e) => {
    if(myState.pdf == null || myState.currentPage > myState.pdf._pdfInfo.numPages) 
    return;
         
    myState.currentPage += 1;
    document.getElementById("current_page").value = myState.currentPage;
    render();
});

最后,我们必须给current_page 文本字段添加一个按键事件监听器,这样用户就可以通过简单地输入页码和点击回车键直接跳到他们想要的任何一页。在事件监听器内,我们需要确保用户输入的数字既大于零,又小于或等于numPages 属性。

document.getElementById('current_page')
    .addEventListener('keypress', (e) => {
    if(myState.pdf == null) return;
 
    // Get key code
    var code = (e.keyCode ? e.keyCode : e.which);
 
    // If key code matches that of the Enter key
    if(code == 13) {
        var desiredPage = document.getElementById('current_page').valueAsNumber;
                         
        if(desiredPage >= 1 && desiredPage <= myState.pdf._pdfInfo.numPages) {
            myState.currentPage = desiredPage;
            document.getElementById("current_page").value = desiredPage;
            render();
        }
    }
});

我们的PDF阅读器现在可以显示PDF文件的任何一页。

Page Controls

6.改变缩放级别

因为我们的render() 函数在渲染页面时已经使用了状态对象的zoom 属性,调整缩放级别就像增加或减少该属性并调用该函数一样简单。

zoom_in 按钮的on-click事件监听器中,让我们将zoom 属性递增为0.5

document.getElementById('zoom_in')
    .addEventListener('click', (e) => {
    if(myState.pdf == null) return;
    myState.zoom += 0.5;

    render();
});

而在zoom_out 按钮的on-click事件监听器中,让我们将zoom 属性递减为0.5

document.getElementById('zoom_out')
    .addEventListener('click', (e) => {
    if(myState.pdf == null) return;
    myState.zoom -= 0.5;
    
    render();
});

你可以自由地给zoom 属性添加上界和下界,但它们不是必须的。

这就是它被放在一起时的样子。

<!doctype html>
 
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My PDF Viewer</title>
  <script
    src="http://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.0.943/pdf.min.js">
  </script>

  <style>
      #canvas_container {
          width: 800px;
          height: 450px;
          overflow: auto;
      }

      #canvas_container {
        background: #333;
        text-align: center;
        border: solid 3px;
      }
  </style>
</head>
<body>
     <div id="my_pdf_viewer">
        <div id="canvas_container">
            <canvas id="pdf_renderer"></canvas>
        </div>

        <div id="navigation_controls">
            <button id="go_previous">Previous</button>
            <input id="current_page" value="1" type="number"/>
            <button id="go_next">Next</button>
        </div>

        <div id="zoom_controls">  
            <button id="zoom_in">+</button>
            <button id="zoom_out">-</button>
        </div>
    </div>

    <script>
        var myState = {
            pdf: null,
            currentPage: 1,
            zoom: 1
        }
     
        pdfjsLib.getDocument('./my_document.pdf').then((pdf) => {
     
            myState.pdf = pdf;
            render();

        });

        function render() {
            myState.pdf.getPage(myState.currentPage).then((page) => {
         
                var canvas = document.getElementById("pdf_renderer");
                var ctx = canvas.getContext('2d');
     
                var viewport = page.getViewport(myState.zoom);

                canvas.width = viewport.width;
                canvas.height = viewport.height;
         
                page.render({
                    canvasContext: ctx,
                    viewport: viewport
                });
            });
        }

        document.getElementById('go_previous').addEventListener('click', (e) => {
            if(myState.pdf == null || myState.currentPage == 1) 
              return;
            myState.currentPage -= 1;
            document.getElementById("current_page").value = myState.currentPage;
            render();
        });

        document.getElementById('go_next').addEventListener('click', (e) => {
            if(myState.pdf == null || myState.currentPage > myState.pdf._pdfInfo.numPages) 
               return;
            myState.currentPage += 1;
            document.getElementById("current_page").value = myState.currentPage;
            render();
        });

        document.getElementById('current_page').addEventListener('keypress', (e) => {
            if(myState.pdf == null) return;
         
            // Get key code
            var code = (e.keyCode ? e.keyCode : e.which);
         
            // If key code matches that of the Enter key
            if(code == 13) {
                var desiredPage = 
                document.getElementById('current_page').valueAsNumber;
                                 
                if(desiredPage >= 1 && desiredPage <= myState.pdf._pdfInfo.numPages) {
                    myState.currentPage = desiredPage;
                    document.getElementById("current_page").value = desiredPage;
                    render();
                }
            }
        });

        document.getElementById('zoom_in').addEventListener('click', (e) => {
            if(myState.pdf == null) return;
            myState.zoom += 0.5;
            render();
        });

        document.getElementById('zoom_out').addEventListener('click', (e) => {
            if(myState.pdf == null) return;
            myState.zoom -= 0.5;
            render();
        });
    </script>
</body>
</html>

我们的PDF阅读器已经准备好了。如果你再次刷新网页,你应该能够查看PDF文件中存在的所有页面,还可以放大或缩小它们。

Zoom Demo

总结

你现在知道如何使用PDF.js为你的网站创建一个自定义的JavaScript PDF浏览器。有了它,你可以自信地提供一个无缝的用户体验,同时显示你的组织的小册子、白皮书、表格和其他一般要作为硬拷贝分发的文件。