【预览PDF】前端预览pdf

【预览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>
相关推荐
90后的晨仔3 小时前
报错 找不到“node”的类型定义文件。 程序包含该文件是因为: 在 compilerOptions 中指定的类型库 "node" 的入口点 。
前端
90后的晨仔3 小时前
5分钟搭建你的第一个TypeScript项目
前端·typescript
专注前端30年3 小时前
Vue2 中 v-if 与 v-show 深度对比及实战指南
开发语言·前端·vue
90后的晨仔3 小时前
TypeScript是什么?为什么前端必须学它?
前端
用户47949283569154 小时前
从 58MB 到 2.6MB:我是如何将 React 官网性能提升 95% 的
前端·javascript
该用户已不存在4 小时前
7个让全栈开发效率起飞的 Bun 工作流
前端·javascript·后端
芙蓉王真的好14 小时前
Angular CDK 响应式工具指南:从基础到自适应布局应用
前端·javascript·angular.js
Boale_H4 小时前
如何获取npm的认证令牌token
前端·npm·node.js
qq_339191144 小时前
vue3 npm run dev局域网可以访问,vue启动设置局域网访问,
前端·vue.js·npm