【預覽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: 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包
<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>
本文章為轉載內容,我們尊重原作者對文章享有的著作權。如有內容錯誤或侵權問題,歡迎原作者聯繫我們進行內容更正或刪除文章。