【预览PDF】前端预览pdf
【预览PDF】前端预览pdf
通过pdfjs-dist预览
注:需要在public中放入对应的pdf.worker.min.mjs文件
<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: stringscale?: number
}>()// 每页 canvas 的 ref 列表
const canvasRefs = ref<HTMLCanvasElement[]>([])
// pdfDoc 用普通变量
let pdfDoc: PDFDocumentProxy | null = null
// 当前加载页
let currentPage = 1
let totalPages = 0// 用 IntersectionObserver 懒加载下一页
let observer: IntersectionObserverconst renderPage = async (pageNum: number) => {if (!pdfDoc) returnconst 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) returncanvas.width = viewport.widthcanvas.height = viewport.heightcanvas.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// 添加到 DOMcanvasRefs.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.promisepdfDoc.getData().then(data => {console.log('🚀 ~ loadPdf ~ data:', data)})totalPages = pdfDoc.numPagescurrentPage = 1canvasRefs.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包
<script setup lang="ts">
import { ref, onMounted, defineProps } from 'vue'
import { init } from '@embedpdf/pdfium'const props = defineProps<{src: stringscale?: number
}>()const containerRef = ref<HTMLDivElement | null>(null)
const loadingRef = ref<boolean>(true)
const errorRef = ref<string | null>(null)const loadPdf = async () => {loadingRef.value = trueerrorRef.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()// 初始化PDFiumconst 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对象的可用方法,帮助我们找出正确的APIconsole.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 = renderWidthcanvas.height = renderHeightcanvas.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 formatif (!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)// 创建ImageDataconst 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 * 4const dstIdx = (y * renderWidth + x) * 4imageData.data[dstIdx] = buffer[srcIdx + 2] // R <- BimageData.data[dstIdx + 1] = buffer[srcIdx + 1] // G <- GimageData.data[dstIdx + 2] = buffer[srcIdx] // B <- RimageData.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} 页渲染完成`)// 添加到DOMif (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.messageloadingRef.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>
