前端实现 PDF 下载地址预览图片

66 阅读4分钟

背景

在发票管理系统中,遇到了一个典型的技术挑战:第三方服务只提供了发票的 PDF 文件下载地址,而业务需求要求在 H5 界面上直接展示发票的图片预览

  • 发票文件由第三方服务提供,后端无法直接获取发票的图片地址
  • 后端只能提供 PDF 文件的下载链接
  • 前端需要在移动端 H5 页面中展示发票预览图
  • 需要支持跨域请求和身份认证(携带 Cookie)

实现方案

1. 安装依赖

npm install pdfjs-dist@5.4.149

2. 关键代码实现

2.1 Worker 配置

PDF.js 使用 Web Worker 进行后台解析,需要正确配置 Worker 路径:

import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.min.mjs';
​
// 配置 PDF.js worker - 使用本地文件
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

注意:使用 new URL()import.meta.url 可以确保在 Vite 构建后 Worker 文件路径正确。

2.2 加载 PDF 文档

加载 PDF 时需要处理跨域和身份认证:

const pdfDoc = await pdfjsLib.getDocument({
    url, // PDF 下载地址
    disableWorker: false,
    disableAutoFetch: false,
    disableStream: false,
    // 配置请求头以携带 cookie
    httpHeaders: {
        'X-Requested-With': 'XMLHttpRequest'
    },
    // 启用跨域请求携带 cookie
    withCredentials: true
}).promise;

关键配置说明

  • withCredentials: true:允许跨域请求携带 Cookie,用于身份认证
  • httpHeaders:自定义请求头,某些服务可能需要特定的请求头

2.3 获取并渲染 PDF 页面

// 获取第一页,发票只有一页
const page = await pdfDoc.getPage(1);
​
// 计算合适的缩放比例
const containerWidth = pdfContainer.value?.clientWidth || 750;
const baseScale = containerWidth / page.getViewport({ scale: 1 }).width;
const devicePixelRatio = window.devicePixelRatio || 1;
const scale = Math.min(baseScale * devicePixelRatio, 3); // 最大3倍缩放
const viewport = page.getViewport({ scale });
​
// 创建 canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
​
// 设置 canvas 的实际尺寸(高分辨率)
canvas.width = viewport.width;
canvas.height = viewport.height;
​
// 设置 canvas 的显示尺寸(CSS 像素)
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.display = 'block';
​
// 启用图像平滑以获得更好的渲染质量
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
​
// 渲染 PDF 页面
const renderContext = {
    canvasContext: context,
    viewport,
    textLayer: false,
    annotationLayer: false,
    enableWebGL: false,
    renderInteractiveForms: false
};
​
await page.render(renderContext).promise;

3.4 清晰度优化

为了在高分辨率屏幕上显示清晰的图片,做了以下优化:

JavaScript 层面

  • 根据设备像素比(devicePixelRatio)动态调整缩放比例
  • 限制最大缩放为 3 倍,避免内存占用过大
  • 启用高质量图像平滑:imageSmoothingQuality = 'high'

CSS 层面

canvas {
    // 确保 canvas 在高分辨率屏幕上显示清晰
    image-rendering: -webkit-optimize-contrast;
    image-rendering: crisp-edges;
    image-rendering: pixelated;
    // 防止图像模糊
    backface-visibility: hidden;
    transform: translateZ(0);
}

遇到的问题和解决方案

问题 1:跨域请求无法携带 Cookie

问题描述:PDF 文件在第三方域名,请求时无法携带 Cookie,导致认证失败。

解决方案

  • getDocument 配置中设置 withCredentials: true
  • 确保服务端设置了正确的 CORS 响应头:Access-Control-Allow-Credentials: true

问题 2:Worker 文件路径错误

问题描述:开发环境正常,但构建后 Worker 文件找不到。

解决方案: 使用 new URL()import.meta.url 动态生成 Worker 路径,确保在构建后路径正确:

pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();

问题 3:Promise.withResolvers 兼容性问题

问题描述:新版本 PDF.js 使用了 Promise.withResolvers(),部分浏览器不支持。

解决方案: 使用 legacy 版本:pdfjs-dist/legacy/build/pdf.min.mjs

完整组件代码

以下是完整的 Vue 3 组件实现:

<template>
    <div class="pdf-viewer">
        <div v-if="loading" class="loading">发票加载中...</div>
        <div v-else-if="error" class="error">发票加载失败</div>
        <div ref="pdfContainer" class="pdf-container" />
    </div>
</template>
​
<script setup>
import { ref, watch, onMounted } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.min.mjs';
  
const props = defineProps({
    url: {
        type: String,
        default: ''
    }
});
​
// 配置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
    import.meta.url
).toString();
​
const pdfContainer = ref(null);
const loading = ref(true);
const error = ref(false);
​
const loadPDF = async() => {
    try {
        
        // 加载 PDF 文档
        const pdfDoc = await pdfjsLib.getDocument({
            props.url, // 发票下载地址
            disableWorker: false,
            disableAutoFetch: false,
            disableStream: false,
            httpHeaders: {
                'X-Requested-With': 'XMLHttpRequest'
            },
            withCredentials: true
        }).promise;
​
        const page = await pdfDoc.getPage(1);
​
        // 设置视口,根据容器宽度自适应
        const containerWidth = pdfContainer.value?.clientWidth || 750;
        const baseScale = containerWidth / page.getViewport({ scale: 1 }).width;
        const devicePixelRatio = window.devicePixelRatio || 1;
        const scale = Math.min(baseScale * devicePixelRatio, 3);
        const viewport = page.getViewport({ scale });
​
        // 创建 canvas
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
​
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        canvas.style.width = '100%';
        canvas.style.height = 'auto';
        canvas.style.display = 'block';
​
        context.imageSmoothingEnabled = true;
        context.imageSmoothingQuality = 'high';
​
        if (pdfContainer.value) {
            pdfContainer.value.innerHTML = '';
            pdfContainer.value.appendChild(canvas);
        }
​
        // 渲染 PDF 页面
        const renderContext = {
            canvasContext: context,
            viewport,
            textLayer: false,
            annotationLayer: false,
            enableWebGL: false,
            renderInteractiveForms: false
        };
​
        await page.render(renderContext).promise;
        loading.value = false;
    } catch (err) {
        console.error('PDF 加载失败:', err);
        error.value = true;
        loading.value = false;
    }
};
​
onMounted(() => {
    loadPDF();
});
</script>
​
<style lang="scss" scoped>
.pdf-viewer {
    width: 100%;
    min-height: 300px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
​
    .loading,
    .error {
        padding: 40px;
        color: #666;
        font-size: 28px;
    }
​
    .error {
        color: #ff6b6b;
    }
​
    .pdf-container {
        width: 100%;
        overflow: hidden;
        display: flex;
        justify-content: center;
        align-items: center;
​
        canvas {
            image-rendering: -webkit-optimize-contrast;
            image-rendering: crisp-edges;
            image-rendering: pixelated;
            backface-visibility: hidden;
            transform: translateZ(0);
        }
    }
}
</style>

reference