PDF 在线预览
GitHub https://www.jsdelivr.com/package/npm/pdfjs-dist
所有版本 https://www.jsdelivr.com/package/npm/pdfjs-dist
1.10.100 版本
https://cdn.jsdelivr.net/npm/pdfjs-dist@1.10.100/build/pdf.min.js
https://cdn.jsdelivr.net/npm/pdfjs-dist@1.10.100/build/pdf.combined.js
https://cdn.jsdelivr.net/npm/pdfjs-dist@1.10.100/build/pdf.worker.min.js
/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;
}