Skip to content

Vue 3 与 pdf-lib 实现浏览器内 PDF 页面缩放 ​

译注:本文译自 dev.to 上 sunshey 的技术文章,原文介绍如何借助 pdf-lib 与 canvas 在浏览器端完成 PDF 页面尺寸调整,涵盖尺寸换算、内容缩放、方向检测与多页处理等实现细节。原文链接见文末。

调整 PDF 页面尺寸——把 Letter 改成 A4,或按目标尺寸缩放内容——听起来像是只有桌面软件才能处理的事情。但借助 pdf-lib、canvas 以及一些坐标计算,你完全可以构建一个纯浏览器端的缩放工具。本文梳理其中的实现细节,包括页面尺寸换算、内容缩放、方向检测以及多页支持。

为什么选择客户端处理 ​

传统 PDF 工具会把文件上传到服务器处理,再把结果传回。对于可能包含个人信息或敏感内容的文档,这会引入不必要的隐私风险。浏览器端方案的优势在于:

  • 所有处理都在本地完成
  • 文件始终留在用户设备上
  • 加载完成后可离线使用
  • 省去服务端带宽成本

技术栈 ​

  • Vue 3 + Composition API
  • pdf-lib 负责 PDF 操作与尺寸调整
  • PDF.js(pdfjs-dist)用于预览渲染
  • Vite 负责打包

添加页面缩放逻辑 ​

核心操作是:读取每一页的原始尺寸,按目标尺寸新建页面,再把原始内容按比例缩放并居中绘制到新页面上。

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, StandardFonts, PdfRange } from 'pdf-lib'

const file = ref<File | null>(null)
const targetSize = ref<'a4' | 'letter' | 'legal' | 'original'>('a4')
const orientation = ref<'auto' | 'portrait' | 'landscape'>('auto')
const marginMm = ref(5)
const applying = ref(false)

// Page size constants in points (1 point = 1/72 inch)
const PAGE_SIZES: Record<string, { width: number; height: number }> = {
  a4:       { width: 595.28, height: 841.89 },
  letter:   { width: 612.00, height: 792.00 },
  legal:    { width: 612.00, height: 1008.00 },
}
</script>

缩放与居中内容 ​

这里是数学计算的关键部分。每一页都需要在保持宽高比的前提下缩放到目标尺寸范围内,然后居中放置。

typescript
async function handleResize() {
  if (!file.value) return
  applying.value = true

  const arrayBuffer = await file.value.arrayBuffer()
  const srcDoc = await PDFDocument.load(arrayBuffer)
  const embedFont = await srcDoc.embedFont(StandardFonts.Helvetica)

  const newDoc = await srcDoc.copy(PdfRange.All)

  const marginPts = marginMm.value * (72 / 25.4)
  const pages = newDoc.getPages()

  for (let i = 0; i < pages.length; i++) {
    const page = pages[i]
    const { width: origW, height: origH } = page.getSize()

    // Determine target dimensions
    let { width: tw, height: th } =
      targetSize.value === 'original'
        ? PAGE_SIZES[origW > origH ? 'letter' : 'a4']
        : PAGE_SIZES[targetSize.value]

    // Handle orientation
    if (orientation.value === 'portrait' && tw > th) {
      ;[tw, th] = [th, tw]
    } else if (orientation.value === 'landscape' && tw < th) {
      ;[tw, th] = [th, tw]
    } else if (orientation.value === 'auto') {
      // Keep existing orientation
      if (origW < origH && tw > th) {
        ;[tw, th] = [th, tw]
      }
    }

    // Scale factor: fit content within available area (minus margins)
    const availW = tw - 2 * marginPts
    const availH = th - 2 * marginPts
    const scale = Math.min(availW / origW, availH / origH, 1)

    // New content dimensions after scaling
    const cw = origW * scale
    const ch = origH * scale

    // Center position
    const cx = (tw - cw) / 2
    const cy = (th - ch) / 2

    page.setSize(tw, th)

    // Draw original page content scaled and positioned
    page.drawImage(newDoc.pages[i], {
      x: cx,
      y: cy,
      width: cw,
      height: ch,
      opacity: 1,
    })
  }

  const result = await newDoc.save()
  downloadResult(result)
  applying.value = false
}

function downloadResult(data: Uint8Array) {
  const blob = new Blob([data], { type: 'application/pdf' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'resized.pdf'
  a.click()
  URL.revokeObjectURL(url)
}

关键难点与解决方案 ​

问题一:在不同 PDFDocument 实例间复制页面 ​

你无法直接跨不同的 PDFDocument 实例使用 page.drawImage(srcPage)。解决方案是使用 pdf-lib 提供的 copy():

typescript
const newDoc = await srcDoc.copy(PdfRange.All)

随后从 newDoc 中获取页面用于绘制,同时以 srcDoc 作为内容来源。

问题二:保留页面旋转标记 ​

有些 PDF 使用 /Rotate 元数据字段,而不是直接交换宽高。在处理之前,需要先规范化旋转:

typescript
for (const pg of pages) {
  const rotate = pg.getRotation().degrees
  if (rotate === 90 || rotate === 270) {
    pg.setRotated(true)
    // Swap dimensions internally
    const { width, height } = pg.getSize()
    pg

(原文此处代码截断,完整实现可参考原文。)

原文链接 ​

https://dev.to/sunshey/how-to-resize-pdf-pages-in-the-browser-with-vue-3-and-pdf-lib-450p