Skip to content

PDF 在线预览

GitHub https://www.jsdelivr.com/package/npm/pdfjs-dist

所有版本 https://www.jsdelivr.com/package/npm/pdfjs-dist

/exp/pdf/base/
html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />

    <title>PDF 预览</title>
  
    <script src="../../assets/pdf/v1.10.100/pdf.min.js"></script>

    <script src="./index.js"></script>

    <link rel="stylesheet" href="./index.css" />
  </head>

  <body> 
    <div class="pdf-container">
      <canvas id="pdf-canvas"></canvas>
      
      <div class="pdf-pagination">
        <button id="pdf-prev" class="pdf-btn">上一页</button>
        <span id="pdf-page-info" class="pdf-page-info">1 / 1</span>
        <button id="pdf-next" class="pdf-btn">下一页</button>
      </div>
    </div>
  </body>
</html>
ts
import { createPDFViewer } from "./pdfv1";

// ==================== 使用示例 ====================

window.addEventListener("DOMContentLoaded", function () {
  console.log("DOMContentLoaded");

  // 创建 PDF 查看器实例
  const pdfViewer = createPDFViewer({
    url: "2020-Scrum-Guide-Chinese-Simplified.pdf",
    canvas: "#pdf-canvas",
    workerSrc: "../../assets/pdf/v1.10.100/pdf.worker.min.js",
    scale: 1.5,
    initialPage: 1,
    onPageRendered: (page, total) => {
      console.log(`页面 ${page}/${total} 渲染完成`);
      // 更新页码显示
      const pageInfo = document.getElementById("pdf-page-info");
      if (pageInfo) {
        pageInfo.textContent = `${page} / ${total}`;
      }
      // 更新按钮状态
      const prevBtn = document.getElementById("pdf-prev") as HTMLButtonElement;
      const nextBtn = document.getElementById("pdf-next") as HTMLButtonElement;
      if (prevBtn) {
        prevBtn.disabled = page <= 1;
      }
      if (nextBtn) {
        nextBtn.disabled = page >= total;
      }
    },
    onLoaded: (total) => {
      console.log(`PDF 加载完成,共 ${total} 页`);
    },
    onError: (error) => {
      console.error("PDF 错误:", error);
    },
  });

  // 外部绑定事件
  const prevBtn = document.getElementById("pdf-prev");
  const nextBtn = document.getElementById("pdf-next");

  if (prevBtn) {
    prevBtn.addEventListener("click", () => {
      pdfViewer.prev();
    });
  }

  if (nextBtn) {
    nextBtn.addEventListener("click", () => {
      pdfViewer.next();
    });
  }

  // 也可以手动控制
  // pdfViewer.next();
  // pdfViewer.prev();
  // pdfViewer.goToPage(5);
  // console.log(pdfViewer.getCurrentPage());
  // console.log(pdfViewer.getTotalPages());
});
ts
// PDF.js 类型声明
declare const PDFJS: {
    workerSrc: string;
    getDocument: (url: string) => Promise<{
        numPages: number;
        getPage: (pageNumber: number) => Promise<{
            getViewport: (scale: number) => { width: number; height: number };
            render: (renderContext: {
                canvasContext: CanvasRenderingContext2D;
                viewport: { width: number; height: number };
            }) => Promise<void>;
        }>;
    }>;
};

// PDF 页面类型
type PDFPage = {
    getViewport: (scale: number) => { width: number; height: number };
    render: (renderContext: {
        canvasContext: CanvasRenderingContext2D;
        viewport: { width: number; height: number };
    }) => Promise<void>;
};

// PDF 文档类型
type PDFDocument = {
    numPages: number;
    getPage: (pageNumber: number) => Promise<PDFPage>;
};

// PDFViewer 配置选项
interface PDFViewerOptions {
    /** PDF 文件 URL */
    url: string;
    /** Canvas 元素或选择器 */
    canvas: HTMLCanvasElement | string;
    /** Worker 文件路径,默认使用相对路径 */
    workerSrc: string;
    /** 缩放比例,默认 1.5 */
    scale?: number;
    /** 初始页码,默认 1 */
    initialPage?: number;
    /** 页面渲染完成回调 */
    onPageRendered?: (pageNumber: number, totalPages: number) => void;
    /** PDF 加载完成回调 */
    onLoaded?: (totalPages: number) => void;
    /** 错误回调 */
    onError?: (error: Error) => void;
}

// PDFViewer 控制接口
interface PDFViewerControls {
    /** 跳转到上一页 */
    prev: () => void;
    /** 跳转到下一页 */
    next: () => void;
    /** 跳转到指定页码 */
    goToPage: (pageNumber: number) => void;
    /** 获取当前页码 */
    getCurrentPage: () => number;
    /** 获取总页数 */
    getTotalPages: () => number;
    /** 重新渲染当前页 */
    render: () => void;
    /** 销毁实例 */
    destroy: () => void;
}

/**
 * 创建 PDF 查看器实例
 * @param options 配置选项
 * @returns PDFViewer 控制对象
 */
export function createPDFViewer(options: PDFViewerOptions): PDFViewerControls {
    // 检查 PDFJS 是否加载
    if (typeof PDFJS === "undefined") {
        throw new Error("PDFJS 未加载,请检查 pdf.min.js 是否正确引入");
    }

    // 解析配置
    const {
        url,
        canvas: canvasInput,
        scale = 1.5,
        workerSrc,
        initialPage = 1,
        onPageRendered,
        onLoaded,
        onError,
    } = options;

    if (!workerSrc) {
        throw new Error("workerSrc 不能为空");
    }

    // 设置 worker
    PDFJS.workerSrc = workerSrc;

    // 解析 canvas
    const canvas =
        typeof canvasInput === "string"
            ? (document.querySelector(canvasInput) as HTMLCanvasElement)
            : canvasInput;

    if (!canvas) {
        throw new Error("找不到 canvas 元素");
    }

    // 状态管理
    let pdfDoc: PDFDocument | null = null;
    let currentPage = initialPage;
    let isRendering = false;

    // 渲染指定页面
    function renderPage(pageNumber: number): Promise<void> {
        // @ts-ignore - Promise 在 es5 中需要类型声明,但运行时支持
        return new Promise<void>((resolve: () => void, reject: (error: Error) => void) => {
            if (!pdfDoc) {
                const error = new Error("PDF 文档未加载");
                reject(error);
                return;
            }

            if (pageNumber < 1 || pageNumber > pdfDoc.numPages) {
                const error = new Error(
                    `页码 ${pageNumber} 超出范围 (1-${pdfDoc.numPages})`
                );
                reject(error);
                return;
            }

            if (isRendering) {
                console.warn("正在渲染中,请稍候...");
                return;
            }

            isRendering = true;
            currentPage = pageNumber;

            pdfDoc!
                .getPage(pageNumber)
                .then((page: PDFPage) => {
                    const viewport = page.getViewport(scale);

                    const context = canvas.getContext("2d");
                    if (!context) {
                        throw new Error("无法获取 canvas 2d 上下文");
                    }

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

                    const renderContext = {
                        canvasContext: context,
                        viewport: viewport,
                    };

                    return page.render(renderContext);
                })
                .then(() => {
                    isRendering = false;
                    if (onPageRendered && pdfDoc) {
                        onPageRendered(currentPage, pdfDoc.numPages);
                    }
                    resolve();
                })
                .catch((error: Error) => {
                    isRendering = false;
                    console.error("页面渲染失败:", error);
                    if (onError) {
                        onError(error);
                    }
                    reject(error);
                });
        });
    }

    // 控制方法
    const controls: PDFViewerControls = {
        prev: () => {
            if (!pdfDoc || currentPage <= 1) return;
            renderPage(currentPage - 1).catch((error) => {
                if (onError) onError(error);
            });
        },

        next: () => {
            if (!pdfDoc || !pdfDoc.numPages) return;
            if (currentPage >= pdfDoc.numPages) return;
            renderPage(currentPage + 1).catch((error) => {
                if (onError) onError(error);
            });
        },

        goToPage: (pageNumber: number) => {
            renderPage(pageNumber).catch((error) => {
                if (onError) onError(error);
            });
        },

        getCurrentPage: () => currentPage,

        getTotalPages: () => (pdfDoc ? pdfDoc.numPages : 0),

        render: () => {
            renderPage(currentPage).catch((error) => {
                if (onError) onError(error);
            });
        },

        destroy: () => {
            pdfDoc = null;
        },
    };

    // 加载 PDF 文档
    PDFJS.getDocument(url)
        .then((pdf: PDFDocument) => {
            console.log("PDF 加载成功,总页数:", pdf.numPages);
            pdfDoc = pdf;

            // 确保初始页码在有效范围内
            const validInitialPage = Math.max(1, Math.min(initialPage, pdf.numPages));
            currentPage = validInitialPage;

            if (onLoaded) {
                onLoaded(pdf.numPages);
            }

            // 渲染初始页
            return renderPage(currentPage);
        })
        .catch((error: Error) => {
            console.error("PDF 加载失败:", error);
            if (onError) {
                onError(error);
            }
        });

    return controls;
}
less
@import "../../my/reset.less";

.pdf-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}

#pdf-canvas {
  height: 22 * 20px;
  display: block;
  margin: 0 auto 20px;
  border: 1px solid #ccc;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.pdf-pagination {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 10px 20px;
  background-color: #f5f5f5;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.pdf-btn {
  padding: 8px 20px;
  font-size: 14px;
  color: #333;
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;

  &:hover:not(:disabled) {
    background-color: #3972D4;
    color: #fff;
    border-color: #3972D4;
  }

  &:active:not(:disabled) {
    transform: scale(0.95);
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

.pdf-page-info {
  font-size: 14px;
  color: #666;
  font-weight: 500;
  min-width: 80px;
  text-align: center;
}