核心原理

  1. PDF 渲染: 浏览器本身无法直接理解PDF的矢量内容(文字、线条、形状),我们需要一个库来读取PDF文件,并将其中的每一页栅格化成一张图片(通常是PNG或JPEG格式)。
  2. 翻页UI: 我们需要一个类似书本或杂志的界面,包含左右两个页面区域,当用户点击“下一页”或“上一页”时,我们切换显示对应的图片。
  3. 交互控制: 监听用户的点击事件、键盘事件(如左右箭头)甚至触摸事件(滑动),来控制页面的切换。

技术选型

实现这个功能,最关键的是选择一个能够将PDF渲染成图片的库,目前最流行和强大的库是 PDF.js

网页以翻页形式阅读pdf源码
(图片来源网络,侵删)
  • PDF.js: 由Mozilla开发,是开源的,它可以直接在浏览器中解析和渲染PDF,无需任何服务器端支持,我们将使用它来将PDF的每一页渲染成Canvas,然后我们可以将Canvas的内容转换成图片URL。

完整实现步骤与源码

我们将创建一个简单的HTML页面,实现以下功能:

  1. 用户通过 <input type="file"> 选择一个本地PDF文件。
  2. 使用PDF.js加载这个文件。
  3. 将PDF的每一页渲染成图片,并存储起来。
  4. 创建一个双页视图,并实现翻页逻辑。

第1步:准备HTML结构

创建一个 index.html 文件,包含基本的页面结构、文件选择器、书本视图区域和控制按钮。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">PDF 翻页阅读器</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>PDF 翻页阅读器</h1>
        <!-- 文件选择器 -->
        <div class="file-input-wrapper">
            <input type="file" id="pdf-upload" accept="application/pdf">
            <label for="pdf-upload">选择PDF文件</label>
        </div>
        <!-- 书本阅读区域 -->
        <div class="book-viewer" id="book-viewer" style="display: none;">
            <div class="book">
                <div class="page left-page" id="left-page">
                    <!-- 左侧页面内容 -->
                </div>
                <div class="page right-page" id="right-page">
                    <!-- 右侧页面内容 -->
                </div>
            </div>
            <!-- 控制按钮 -->
            <div class="controls">
                <button id="prev-btn">上一页</button>
                <span id="page-info">0 / 0</span>
                <button id="next-btn">下一页</button>
            </div>
        </div>
    </div>
    <!-- 引入 PDF.js 库 -->
    <!-- 使用官方的CDN链接 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
    <!-- 设置PDF.js worker的路径 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
    <script src="script.js"></script>
</body>
</html>

第2步:添加CSS样式

创建一个 style.css 文件,让我们的书本看起来更美观。

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f0f2f5;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}
.container {
    text-align: center;
    width: 90%;
    max-width: 900px;
}
.file-input-wrapper {
    margin-bottom: 20px;
}
.file-input-wrapper input[type=file] {
    display: none;
}
.file-input-wrapper label {
    display: inline-block;
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
.file-input-wrapper label:hover {
    background-color: #0056b3;
}
.book-viewer {
    margin-top: 30px;
}
.book {
    display: flex;
    justify-content: center;
    gap: 20px; /* 页面之间的缝隙 */
    perspective: 2000px; /* 3D效果 */
    margin-bottom: 20px;
}
.page {
    width: 400px;
    height: 600px;
    background-color: #fff;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
    background-image: linear-gradient(#e6e6e6 1px, transparent 1px),
                      linear-gradient(90deg, #e6e6e6 1px, transparent 1px);
    background-size: 20px 20px;
}
.page img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.controls {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 15px;
}
.controls button {
    padding: 8px 15px;
    background-color: #28a745;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
.controls button:hover {
    background-color: #218838;
}
.controls button:disabled {
    background-color: #6c757d;
    cursor: not-allowed;
}
#page-info {
    font-size: 1.1em;
    font-weight: bold;
}

第3步:编写JavaScript逻辑

这是最核心的部分,创建 script.js 文件,实现PDF加载、页面渲染和翻页控制。

网页以翻页形式阅读pdf源码
(图片来源网络,侵删)
document.addEventListener('DOMContentLoaded', () => {
    const pdfUpload = document.getElementById('pdf-upload');
    const bookViewer = document.getElementById('book-viewer');
    const leftPage = document.getElementById('left-page');
    const rightPage = document.getElementById('right-page');
    const prevBtn = document.getElementById('prev-btn');
    const nextBtn = document.getElementById('next-btn');
    const pageInfo = document.getElementById('page-info');
    let pdfDoc = null;
    let pageNum = 1;
    let pageRendering = false;
    let pageNumPending = null;
    let scale = 1.5; // 缩放比例,可以根据需要调整
    let canvas = document.createElement('canvas'); // 创建一个离屏canvas用于渲染
    let ctx = canvas.getContext('2d');
    let pagesImages = []; // 存储所有页面渲染后的图片URL
    // 渲染页面函数
    const renderPage = (num) => {
        pageRendering = true;
        // 获取指定页面的PDF对象
        pdfDoc.getPage(num).then(page => {
            const viewport = page.getViewport({ scale: scale });
            // 设置canvas尺寸
            canvas.height = viewport.height;
            canvas.width = viewport.width;
            // 渲染PDF页面到canvas
            const renderContext = {
                canvasContext: ctx,
                viewport: viewport
            };
            const renderTask = page.render(renderContext);
            // 等待渲染完成
            renderTask.promise.then(() => {
                // 将canvas内容转换为图片URL并存储
                const imgData = canvas.toDataURL('image/png');
                pagesImages[num - 1] = imgData; // 数组索引从0开始
                pageRendering = false;
                if (pageNumPending !== null) {
                    // 如果有等待渲染的页面,则渲染它
                    renderPage(pageNumPending);
                    pageNumPending = null;
                }
                // 更新显示
                updatePageView();
            });
        });
    };
    // 如果有另一个页面在渲染,则将当前页面加入队列
    const queueRenderPage = (num) => {
        if (pageRendering) {
            pageNumPending = num;
        } else {
            renderPage(num);
        }
    };
    // 更新页面显示
    const updatePageView = () => {
        // 清空当前页面
        leftPage.innerHTML = '';
        rightPage.innerHTML = '';
        // 双页模式:显示当前页和下一页
        if (pdfDoc) {
            // 左侧页
            if (pageNum >= 1) {
                const leftImg = document.createElement('img');
                leftImg.src = pagesImages[pageNum - 1];
                leftPage.appendChild(leftImg);
            }
            // 右侧页
            if (pageNum + 1 <= pdfDoc.numPages) {
                const rightImg = document.createElement('img');
                rightImg.src = pagesImages[pageNum];
                rightPage.appendChild(rightImg);
            }
        }
        // 更新页面信息
        pageInfo.textContent = `${pageNum} / ${pdfDoc.numPages}`;
        // 更新按钮状态
        prevBtn.disabled = pageNum <= 1;
        nextBtn.disabled = pageNum >= pdfDoc.numPages;
    };
    // 上一页
    const onPrevPage = () => {
        if (pageNum <= 1) {
            return;
        }
        pageNum--;
        updatePageView();
    };
    // 下一页
    const onNextPage = () => {
        if (pageNum >= pdfDoc.numPages) {
            return;
        }
        pageNum++;
        updatePageView();
    };
    // 事件监听
    prevBtn.addEventListener('click', onPrevPage);
    nextBtn.addEventListener('click', onNextPage);
    // 键盘事件监听
    document.addEventListener('keydown', (e) => {
        if (bookViewer.style.display === 'none') return;
        if (e.key === 'ArrowLeft') {
            onPrevPage();
        } else if (e.key === 'ArrowRight') {
            onNextPage();
        }
    });
    // 文件上传处理
    pdfUpload.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (file && file.type === 'application/pdf') {
            const fileReader = new FileReader();
            fileReader.onload = () => {
                const typedarray = new Uint8Array(fileReader.result);
                // 使用PDF.js加载PDF
                pdfjsLib.getDocument(typedarray).promise.then(pdf_ => {
                    pdfDoc = pdf_;
                    console.log(`PDF loaded: ${pdfDoc.numPages} pages`);
                    // 初始化所有页面的图片存储数组
                    pagesImages = new Array(pdfDoc.numPages);
                    // 渲染前两页
                    for (let i = 1; i <= Math.min(2, pdfDoc.numPages); i++) {
                        queueRenderPage(i);
                    }
                    // 显示书本阅读器
                    bookViewer.style.display = 'block';
                    updatePageView();
                }).catch(error => {
                    console.error('Error loading PDF:', error);
                    alert('PDF加载失败,请检查文件是否损坏。');
                });
            };
            fileReader.readAsArrayBuffer(file);
        } else {
            alert('请选择一个有效的PDF文件。');
        }
    });
});

如何运行

  1. index.html, style.css, 和 script.js 这三个文件放在同一个文件夹下。
  2. 用浏览器打开 index.html 文件。
  3. 点击“选择PDF文件”按钮,选择你电脑上的一个PDF文件。
  4. 文件加载后,就会以翻页的形式显示在页面上。

代码解释与扩展

  1. script.js 中的 pdfjsLib: 我们通过CDN引入了PDF.js,所以可以直接使用全局的 pdfjsLib 对象。
  2. 离屏Canvas: 我们创建了一个不在DOM中的 <canvas> 元素,这样做的好处是,我们可以在后台默默地渲染页面,而不会干扰页面的布局和显示,渲染完成后,我们将其转换为 dataURL (base64编码的图片) 存储在 pagesImages 数组中。
  3. queueRenderPage: 这是一个优化,由于PDF渲染是异步操作,如果用户快速点击翻页,可能会导致多个渲染任务同时进行,造成混乱,这个函数确保了任何时候只有一个页面在渲染,其他的会被排队等待。
  4. 双页视图: updatePageView 函数根据当前页码 pageNum,决定左右两个 <div> 中应该显示哪两张图片,这是实现书本样式的关键。
  5. 扩展功能:
    • 触摸滑动: 可以使用 touchstarttouchend 事件来检测用户的滑动方向,实现触摸屏设备的翻页。
    • 加载进度条: 在渲染所有页面之前,可以显示一个进度条,告知用户正在准备。
    • 缩放功能: 添加按钮来增加或减少 scale 变量,然后重新渲染页面,可以实现放大和缩小。
    • 目录/书签: PDF.js 也可以提取PDF的目录信息,你可以将其显示在侧边栏,实现快速跳转。

这个例子为你提供了一个坚实的基础,你可以基于它进行更多的定制和功能扩展。

网页以翻页形式阅读pdf源码
(图片来源网络,侵删)