【预览PDF】前端预览pdf
通过pdfjs-dist预览
注:需要在public中放入对应的pdf.worker.min.mjs文件
html
<script setup lang="ts">
import { ref, onMounted, defineProps } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api'
// Worker 必须存在 public/pdf.worker.min.mjs
pdfjsLib.GlobalWorkerOptions.workerSrc = window.location.origin + '/pdf.worker.min.mjs'
const props = defineProps<{
src: string
scale?: number
}>()
// 每页 canvas 的 ref 列表
const canvasRefs = ref<HTMLCanvasElement[]>([])
// pdfDoc 用普通变量
let pdfDoc: PDFDocumentProxy | null = null
// 当前加载页
let currentPage = 1
let totalPages = 0
// 用 IntersectionObserver 懒加载下一页
let observer: IntersectionObserver
const renderPage = async (pageNum: number) => {
if (!pdfDoc) return
const page = await pdfDoc.getPage(pageNum)
// const viewport = page.getViewport({ scale: props.scale ?? 1 })
const viewport = page.getViewport({ scale: props.scale ?? 1.1 })
// 创建 canvas 元素
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) return
canvas.width = viewport.width
canvas.height = viewport.height
canvas.style.width = '100%' // 自适应父容器宽度
canvas.style.height = 'auto'
canvas.style.display = 'block'
canvas.style.marginBottom = '16px'
const renderContext = {
canvas,
canvasContext: context,
viewport,
renderInteractiveForms: true,
textLayer: true
}
await page.render(renderContext).promise
// 添加到 DOM
canvasRefs.value.push(canvas)
containerRef.value?.appendChild(canvas)
// 观察最后一页,滚动到可见时加载下一页
if (pageNum === currentPage && currentPage < totalPages) {
observer.observe(canvas)
}
}
const loadPdf = async () => {
const loadingTask = pdfjsLib.getDocument({
url: props.src,
cMapUrl: window.location.origin + '/pdf/cmaps/',
cMapPacked: true,
enableXfa: true,
// 添加更多参数以提高兼容性
disableFontFace: false,
rangeChunkSize: 65536,
maxImageSize: -1
// nativeImageDecoderSupport: true
})
try {
pdfDoc = await loadingTask.promise
pdfDoc.getData().then(data => {
console.log('🚀 ~ loadPdf ~ data:', data)
})
totalPages = pdfDoc.numPages
currentPage = 1
canvasRefs.value = []
containerRef.value!.innerHTML = '' // 清空之前内容
await renderPage(currentPage)
} catch (err) {
console.error('🚀 ~ loadPdf ~ err:', err)
}
}
const containerRef = ref<HTMLDivElement | null>(null)
onMounted(() => {
observer = new IntersectionObserver(
async entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
observer.unobserve(entry.target) // 防止重复触发
currentPage++
if (currentPage <= totalPages) {
await renderPage(currentPage)
}
}
}
},
{
root: containerRef.value,
rootMargin: '0px',
threshold: 0.1
}
)
loadPdf()
})
</script>
<template>
<div ref="containerRef" class="w-full overflow-auto" style="max-height: 80vh">
<!-- canvas 会动态加入这里 -->
</div>
</template>
<style scoped>
canvas {
background-color: #fff;
color: #000;
}
</style>
通过@embedpdf/pdfium预览
注:需要下载对应的pdfium.wasm包
html
<script setup lang="ts">
import { ref, onMounted, defineProps } from 'vue'
import { init } from '@embedpdf/pdfium'
const props = defineProps<{
src: string
scale?: number
}>()
const containerRef = ref<HTMLDivElement | null>(null)
const loadingRef = ref<boolean>(true)
const errorRef = ref<string | null>(null)
const loadPdf = async () => {
loadingRef.value = true
errorRef.value = null
// 清空容器
if (containerRef.value) {
containerRef.value.innerHTML = ''
}
try {
console.log('开始加载PDFium WebAssembly...')
// 从CDN加载WebAssembly文件
const pdfiumWasmUrl = 'https://cdn.jsdelivr.net/npm/@embedpdf/pdfium/dist/pdfium.wasm'
const response = await fetch(pdfiumWasmUrl)
const wasmBinary = await response.arrayBuffer()
// 初始化PDFium
const pdfium = await init({ wasmBinary })
console.log('PDFium WebAssembly加载成功')
// 初始化PDFium扩展库
pdfium.PDFiumExt_Init()
console.log('PDFium扩展库初始化成功')
// 加载PDF文件
console.log('开始加载PDF文件:', props.src)
const pdfResponse = await fetch(props.src)
const pdfBuffer = await pdfResponse.arrayBuffer()
const pdfData = new Uint8Array(pdfBuffer)
console.log(`PDF文件加载成功,大小: ${pdfData.length} 字节`)
// 分配内存并加载文档
const filePtr = pdfium.pdfium.wasmExports.malloc(pdfData.length)
pdfium.pdfium.HEAPU8.set(pdfData, filePtr)
// 加载PDF文档
const docPtr = pdfium.FPDF_LoadMemDocument(filePtr, pdfData.length, '')
if (!docPtr) {
const errorCode = pdfium.FPDF_GetLastError()
throw new Error(`PDF文档加载失败,错误码: ${errorCode}`)
}
console.log('PDF文档加载成功')
// 获取页数
const pageCount = pdfium.FPDF_GetPageCount(docPtr)
console.log(`PDF共有 ${pageCount} 页`)
if (pageCount === 0) {
throw new Error('PDF没有任何页面')
}
// 添加加载指示器
const loadingIndicator = document.createElement('div')
loadingIndicator.style.padding = '20px'
loadingIndicator.style.textAlign = 'center'
loadingIndicator.textContent = 'PDF加载中...'
if (containerRef.value) {
containerRef.value.appendChild(loadingIndicator)
}
// 记录一下pdfium对象的可用方法,帮助我们找出正确的API
console.log(
'PDFium可用方法:',
Object.keys(pdfium).filter(key => typeof pdfium[key] === 'function')
)
// 逐页渲染
for (let i = 0; i < pageCount; i++) {
try {
console.log(`开始渲染第 ${i + 1} 页...`)
// 加载页面
const pagePtr = pdfium.FPDF_LoadPage(docPtr, i)
if (!pagePtr) {
throw new Error(`无法加载第 ${i + 1} 页`)
}
// 获取页面尺寸
const origWidth = pdfium.FPDF_GetPageWidth(pagePtr)
const origHeight = pdfium.FPDF_GetPageHeight(pagePtr)
console.log(`页面原始尺寸: ${origWidth} x ${origHeight}`)
// 使用固定的缩放比例或用户提供的缩放比例
const scale = props.scale || 1.5
// 计算实际渲染尺寸
const renderWidth = Math.floor(origWidth * scale)
const renderHeight = Math.floor(origHeight * scale)
console.log(`渲染尺寸: ${renderWidth} x ${renderHeight}`)
// 创建canvas元素
const canvas = document.createElement('canvas')
canvas.width = renderWidth
canvas.height = renderHeight
canvas.style.width = '100%'
canvas.style.height = 'auto'
canvas.style.display = 'block'
canvas.style.marginBottom = '20px'
canvas.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.15)'
// 获取canvas上下文
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('无法获取canvas上下文')
}
// 设置白色背景
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, renderWidth, renderHeight)
// 检查FPDFBitmap_Create是否可用(替代PDFiumExt_CreateBitmap)
if (typeof pdfium.FPDFBitmap_Create === 'function') {
console.log('使用FPDFBitmap_Create创建位图')
// 创建位图
const bitmapPtr = pdfium.FPDFBitmap_Create(renderWidth, renderHeight, 1) // 1 for BGRA format
if (!bitmapPtr) {
throw new Error('无法创建位图')
}
// 填充位图背景为白色 (0xFFFFFFFF 为白色,BGRA格式)
pdfium.FPDFBitmap_FillRect(bitmapPtr, 0, 0, renderWidth, renderHeight, 0xffffffff)
// 渲染PDF页面到位图
pdfium.FPDF_RenderPageBitmap(
bitmapPtr,
pagePtr,
0,
0,
renderWidth,
renderHeight,
0, // 旋转角度
pdfium.FPDF_RENDER_ANNOT // 渲染标志
)
// 获取位图缓冲区
const scanline = pdfium.FPDFBitmap_GetStride(bitmapPtr)
const bufferPtr = pdfium.FPDFBitmap_GetBuffer(bitmapPtr)
// 创建ImageData
const buffer = new Uint8ClampedArray(pdfium.pdfium.HEAPU8.buffer, bufferPtr, scanline * renderHeight)
// 处理BGRA到RGBA的转换(如果需要)
const imageData = new ImageData(renderWidth, renderHeight)
for (let y = 0; y < renderHeight; y++) {
for (let x = 0; x < renderWidth; x++) {
const srcIdx = y * scanline + x * 4
const dstIdx = (y * renderWidth + x) * 4
imageData.data[dstIdx] = buffer[srcIdx + 2] // R <- B
imageData.data[dstIdx + 1] = buffer[srcIdx + 1] // G <- G
imageData.data[dstIdx + 2] = buffer[srcIdx] // B <- R
imageData.data[dstIdx + 3] = buffer[srcIdx + 3] // A <- A
}
}
ctx.putImageData(imageData, 0, 0)
// 释放位图
pdfium.FPDFBitmap_Destroy(bitmapPtr)
} else {
// 备选方案:使用原生canvas渲染(如果可用)
console.log('尝试使用FPDF_RenderPageToDC进行canvas渲染')
// 创建一个临时的img元素,用于存放渲染结果
const img = new Image()
// 使用FPDF_RenderPage直接渲染到canvas(如果API支持)
if (typeof pdfium.FPDF_RenderPage === 'function') {
console.log('使用FPDF_RenderPage直接渲染')
pdfium.FPDF_RenderPage(ctx, pagePtr, 0, 0, renderWidth, renderHeight, 0, 0)
} else {
throw new Error('无法找到适合的渲染API,请检查PDFium库版本')
}
}
console.log(`第 ${i + 1} 页渲染完成`)
// 添加到DOM
if (containerRef.value) {
if (i === 0) {
// 如果是第一页,移除加载指示器
containerRef.value.innerHTML = ''
}
containerRef.value.appendChild(canvas)
}
// 关闭页面
pdfium.FPDF_ClosePage(pagePtr)
} catch (pageError) {
console.error(`渲染第 ${i + 1} 页时出错:`, pageError)
// 创建错误提示元素
const errorDiv = document.createElement('div')
errorDiv.style.width = '100%'
errorDiv.style.padding = '15px'
errorDiv.style.marginBottom = '20px'
errorDiv.style.backgroundColor = '#f8d7da'
errorDiv.style.border = '1px solid #f5c6cb'
errorDiv.style.color = '#721c24'
errorDiv.style.borderRadius = '4px'
errorDiv.textContent = `无法渲染第 ${i + 1} 页: ${pageError.message}`
if (containerRef.value) {
if (i === 0) {
// 如果是第一页出错,清空容器
containerRef.value.innerHTML = ''
}
containerRef.value.appendChild(errorDiv)
}
}
}
console.log('所有页面渲染完成')
// 清理资源
pdfium.FPDF_CloseDocument(docPtr)
pdfium.pdfium.wasmExports.free(filePtr)
loadingRef.value = false
} catch (error) {
console.error('PDF处理失败:', error)
errorRef.value = error.message
loadingRef.value = false
// 显示错误信息
if (containerRef.value) {
const errorContainer = document.createElement('div')
errorContainer.style.width = '100%'
errorContainer.style.padding = '20px'
errorContainer.style.backgroundColor = '#f8d7da'
errorContainer.style.border = '1px solid #f5c6cb'
errorContainer.style.color = '#721c24'
errorContainer.style.borderRadius = '4px'
errorContainer.textContent = `PDF加载或渲染失败: ${error.message}`
containerRef.value.innerHTML = ''
containerRef.value.appendChild(errorContainer)
}
}
}
onMounted(() => {
loadPdf()
})
</script>
<template>
<div class="pdf-viewer">
<div v-if="loadingRef && !errorRef" class="pdf-loading">加载中...</div>
<div v-if="errorRef" class="pdf-error">
{{ errorRef }}
</div>
<div ref="containerRef" class="pdf-container">
<!-- PDF页面将被渲染到这里 -->
</div>
</div>
</template>
<style scoped>
.pdf-viewer {
width: 100%;
position: relative;
}
.pdf-container {
width: 100%;
overflow-y: auto;
max-height: 80vh;
background-color: #f5f5f5;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.pdf-loading,
.pdf-error {
padding: 20px;
text-align: center;
}
.pdf-error {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin-bottom: 15px;
}
</style>