跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
|注册
博客列表

目录

  1. 核心功能亮点
  2. 完整 Vue 3 + TypeScript 代码实现
  3. 压缩效果对比
  4. 典型使用场景
  5. 如何使用(三步完成压缩)
  6. 总结
TypeScript大前端

PDF 压缩工具:纯前端开源本地压缩方案及实现思路

本文介绍了一款基于纯前端技术的 PDF 压缩工具,采用 Vue 3 和 TypeScript 开发。核心功能包括多级压缩质量预设、精细图片质量控制、批量处理及实时反馈。所有处理在浏览器本地完成,确保隐私安全。文章提供了完整的代码实现,涵盖 pdf.js 和 pdf-lib 库的使用,展示了如何通过降低图片质量和移除冗余数据来减小文件体积,适用于邮件附件优化、文档存储节省等场景。

孤勇者发布于 2026/3/260 浏览
PDF 压缩工具:纯前端开源本地压缩方案及实现思路

在日常办公和文件管理中,PDF 文件体积过大常常带来诸多困扰:邮件附件发送受限、云端存储空间紧张、文档传输耗时过长。一个安全、高效且不泄露隐私的 PDF 压缩工具,成为许多用户和开发者的刚需。

为此,一款完全开源的 PDF 压缩工具应运而生。它采用先进的纯前端技术,通过智能优化图片质量和移除冗余数据,在保证可读性的前提下显著减小 PDF 文件体积。所有处理均在浏览器本地完成,确保您的文档隐私零泄露。

核心功能亮点

  1. 智能压缩,灵活可控
    • 多级压缩质量:提供高质量(较小压缩)、中等质量(推荐)和低质量(最大压缩)三种预设,满足不同场景需求。
    • 精细图片质量控制:通过直观的滑块(10%-100%),可精确控制 PDF 中图片的压缩程度,在文件大小和视觉效果间找到最佳平衡。
  2. 批量处理,高效便捷
    • 支持同时选择多个 PDF 文件进行压缩,大幅提升处理效率。
    • 压缩完成后,可单独下载每个文件,或一键打包下载所有压缩后的文件。
  3. 实时反馈,清晰透明
    • 每个文件都显示原始大小、压缩后大小和压缩比例。
    • 压缩比例以颜色标识(高绿、中黄、低灰),直观呈现压缩效果。
    • 处理状态(等待中、压缩中、完成、失败)一目了然。
  4. 隐私安全,纯前端处理
    • 所有压缩操作均在您的浏览器本地完成,PDF 文件数据全程不会上传至任何服务器。
    • 基于 pdf.js 和 pdf-lib 两大专业库实现,确保处理过程的准确性和可靠性。
  5. 广泛兼容,智能处理
    • 支持大部分标准 PDF 格式,通过智能渲染和重编码技术实现有效压缩。
    • 注:加密或损坏的 PDF 可能无法正常处理,系统会给出明确提示。

完整 Vue 3 + TypeScript 代码实现

以下是该工具的核心代码实现,基于 Vue 3、TypeScript、Element Plus 组件库,并集成了 pdf.js、pdf-lib、JSZip 和 FileSaver.js 等专业库:

<template>
  <div>
    <DetailHeader :title="info.title"></DetailHeader>
    <div>
      <div>
        <el-text type="info">压缩 PDF 文档大小,支持批量压缩,所有操作在本地完成。</el-text>
      </div>
      <div>
        <el-text type="info">通过降低图片质量和移除冗余数据来减小 PDF 文件体积。</el-text>
      </div>
      <div>
        <div>
          <span>压缩质量:</span>
          <el-select v-model="compressQuality">
            <el-option label="高质量 (较小压缩)" value="high" />
            <el-option label="中等质量 (推荐)" value="medium" />
            <el-option label="低质量 (最大压缩)" value="low" />
          </el-select>
        </div>
        <div>
          <span>图片质量:</span>
          <el-slider v-model="imageQuality" :min="10" :max="100" :format-tooltip="(val: number) => `${val}%`" />
        </div>
      </div>
      <div>
        <el-upload accept=".pdf" :auto-upload="false" :on-change="onFileChange" :multiple="true" :show-file-list="false">
          <el-button type="primary">选择 PDF 文件</el-button>
        </el-upload>
      </div>
      <div v-if="files.length">
        <div>
          <el-button type="success" @click="compressAllFiles" :disabled="loading">开始压缩</el-button>
          <el-button type="warning" @click="clearFiles" :disabled="loading">清空文件</el-button>
        </div>
        <div>
          <div v-for="(file, idx) in files" :key="file.uid">
            <div>
              <span>{{ file.name }}</span>
              <div>
                <span>原始:{{ formatFileSize(file.size) }}</span>
                <span v-if="file.compressedSize"> 压缩后:{{ formatFileSize(file.compressedSize) }} </span>
                <span v-if="file.compressedSize" :class="getRatioClass(file)"> ({{ getRatio(file) }}) </span>
              </div>
            </div>
            <div>
              <el-button v-if="file.compressedUrl" type="primary" size="small" @click="downloadFile(file)">下载</el-button>
              <el-button type="danger" size="small" @click="removeFile(idx)">删除</el-button>
            </div>
            <div v-if="file.status === 'processing'">
              <el-icon><Loading /></el-icon>
              <span>压缩中...</span>
            </div>
            <div v-else-if="file.status === 'done'">
              <el-icon><CircleCheck /></el-icon>
              <span>完成</span>
            </div>
            <div v-else-if="file.status === 'error'">
              <el-icon><CircleClose /></el-icon>
              <span>失败</span>
            </div>
          </div>
        </div>
      </div>
      <div v-if="loading">
        <el-alert type="info" :title="loadingMsg" show-icon :closable="false" />
        <el-progress :percentage="progress" :format="() => `${progress}%`" />
      </div>
      <div v-if="errorMsg">
        <el-alert type="error" :title="errorMsg" show-icon :closable="false" />
      </div>
      <div v-if="completedCount > 0">
        <el-alert type="success" :title="`成功压缩 ${completedCount} 个文件`" show-icon :closable="false" />
        <div>
          <el-button type="primary" @click="downloadAllFiles">全部下载</el-button>
        </div>
      </div>
    </div>
    <ToolDetail title="使用说明">
      <el-text>
        <ul>
          <li>点击'选择 PDF 文件'按钮,选择一个或多个需要压缩的 PDF 文件。</li>
          <li>选择压缩质量:高质量保留更多细节,低质量压缩比更大。</li>
          <li>调整图片质量滑块,控制 PDF 中图片的压缩程度。</li>
          <li>点击'开始压缩'按钮开始压缩过程。</li>
          <li>压缩完成后,可以单独下载每个文件或批量下载所有文件。</li>
          <li>所有压缩操作均在本地完成,不会上传到服务器。</li>
          <li>支持大部分标准 PDF 格式,加密或损坏的 PDF 可能无法正常压缩。</li>
        </ul>
      </el-text>
    </ToolDetail>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import DetailHeader from '@/components/Layout/DetailHeader/DetailHeader.vue';
import ToolDetail from '@/components/Layout/ToolDetail/ToolDetail.vue';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min?url';
import { PDFDocument } from 'pdf-lib';
import { Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

const info = reactive({ title: "📦 PDF 压缩工具", });

interface FileItem {
  uid: string;
  name: string;
  size: number;
  raw: File;
  status: 'pending' | 'processing' | 'done' | 'error';
  compressedSize?: number;
  compressedUrl?: string;
  compressedBytes?: Uint8Array;
}

const files = ref<FileItem[]>([]);
const loading = ref(false);
const loadingMsg = ref('');
const progress = ref(0);
const errorMsg = ref('');
const compressQuality = ref('medium');
const imageQuality = ref(70);
const completedCount = computed(() => files.value.filter(f => f.status === 'done').length);

const onFileChange = (file: any) => {
  if (!file || !file.raw) return;
  if (file.raw.type !== 'application/pdf') {
    errorMsg.value = '请选择 PDF 文件';
    return;
  }
  errorMsg.value = '';
  const newFile: FileItem = {
    uid: Date.now().toString() + Math.random().toString(36).substr(2, 9),
    name: file.name,
    size: file.size,
    raw: file.raw,
    status: 'pending'
  };
  files.value.push(newFile);
};

const formatFileSize = (size: number): string => {
  if (size < 1024) return size + ' B';
  if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
  return (size / (1024 * 1024)).toFixed(2) + ' MB';
};

const getRatio = (file: FileItem): string => {
  if (!file.compressedSize) return '';
  const ratio = ((1 - file.compressedSize / file.size) * 100).toFixed(1);
  return ratio + '% 减少';
};

const getRatioClass = (file: FileItem): string => {
  if (!file.compressedSize) return '';
  const ratio = (1 - file.compressedSize / file.size) * 100;
  if (ratio >= 50) return 'ratio-high';
  if (ratio >= 20) return 'ratio-medium';
  return 'ratio-low';
};

const removeFile = (idx: number) => {
  const file = files.value[idx];
  if (file.compressedUrl) {
    URL.revokeObjectURL(file.compressedUrl);
  }
  files.value.splice(idx, 1);
};

const clearFiles = () => {
  files.value.forEach(file => {
    if (file.compressedUrl) {
      URL.revokeObjectURL(file.compressedUrl);
    }
  });
  files.value = [];
  errorMsg.value = '';
};

const compressPDF = async (file: FileItem): Promise<void> => {
  const arrayBuffer = await file.raw.arrayBuffer();
  const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer.slice(0) }).promise;
  const numPages = pdfJsDoc.numPages;
  const newPdfDoc = await PDFDocument.create();
  const quality = imageQuality.value / 100;
  const scale = getScaleByQuality();

  for (let i = 1; i <= numPages; i++) {
    const page = await pdfJsDoc.getPage(i);
    const viewport = page.getViewport({ scale });
    const canvas = document.createElement('canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const context = canvas.getContext('2d')!;
    context.fillStyle = '#FFFFFF';
    context.fillRect(0, 0, canvas.width, canvas.height);
    await page.render({ canvasContext: context, viewport, canvas, }).promise;
    const imgDataUrl = canvas.toDataURL('image/jpeg', quality);
    const imgBytes = await fetch(imgDataUrl).then(r => r.arrayBuffer());
    const img = await newPdfDoc.embedJpg(imgBytes);
    const pdfPage = newPdfDoc.addPage([viewport.width, viewport.height]);
    pdfPage.drawImage(img, { x: 0, y: 0, width: viewport.width, height: viewport.height, });
  }

  const compressedBytes = await newPdfDoc.save({ useObjectStreams: true, addDefaultPage: false, objectsPerTick: 50, });
  const compressedSize = compressedBytes.length;
  const blob = new Blob([new Uint8Array(compressedBytes)], { type: 'application/pdf' });
  const compressedUrl = URL.createObjectURL(blob);
  file.compressedSize = compressedSize;
  file.compressedUrl = compressedUrl;
  file.compressedBytes = compressedBytes;
};

const getScaleByQuality = (): number => {
  switch (compressQuality.value) {
    case 'high': return 1.5;
    case 'low': return 0.8;
    case 'medium': default: return 1.2;
  }
};

const compressAllFiles = async () => {
  if (files.value.length === 0) {
    errorMsg.value = '请先选择 PDF 文件';
    return;
  }
  loading.value = true;
  errorMsg.value = '';
  progress.value = 0;
  const pendingFiles = files.value.filter(f => f.status === 'pending' || f.status === 'error');

  for (let i = 0; i < pendingFiles.length; i++) {
    const file = pendingFiles[i];
    file.status = 'processing';
    loadingMsg.value = `正在压缩:${file.name} (${i + 1}/${pendingFiles.length})`;
    try {
      await compressPDF(file);
      file.status = 'done';
    } catch (err) {
      file.status = 'error';
      console.error(`压缩失败:${file.name}`, err);
    }
    progress.value = Math.round(((i + 1) / pendingFiles.length) * 100);
  }
  loading.value = false;
  loadingMsg.value = '';
};

const downloadFile = (file: FileItem) => {
  if (!file.compressedUrl) return;
  const link = document.createElement('a');
  link.href = file.compressedUrl;
  const baseName = file.name.replace('.pdf', '');
  link.download = `${baseName}_compressed.pdf`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};

const downloadAllFiles = async () => {
  const completedFiles = files.value.filter(f => f.status === 'done' && f.compressedBytes);
  if (completedFiles.length === 0) {
    errorMsg.value = '没有可下载的文件';
    return;
  }
  if (completedFiles.length === 1) {
    downloadFile(completedFiles[0]);
    return;
  }
  loading.value = true;
  loadingMsg.value = '正在打包文件...';
  try {
    const zip = new JSZip();
    completedFiles.forEach(file => {
      const baseName = file.name.replace('.pdf', '');
      zip.file(`${baseName}_compressed.pdf`, file.compressedBytes!);
    });
    const blob = await zip.generateAsync({ type: 'blob' });
    saveAs(blob, 'compressed_pdfs.zip');
  } catch (err) {
    errorMsg.value = '打包下载失败';
    console.error(err);
  } finally {
    loading.value = false;
    loadingMsg.value = '';
  }
};
</script>

<style scoped>
.settings-section { padding: 16px; background: #f8f9fa; border-radius: 8px; }
.settings-row { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; }
.setting-item { display: flex; align-items: center; gap: 8px; }
.setting-label { font-size: 14px; color: #606266; white-space: nowrap; }
.file-list { margin-top: 18px; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; }
.file-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid #e4e7ed; background: #fafbfc; transition: background-color 0.2s; flex-wrap: wrap; gap: 12px; }
.file-item:hover { background: #f0f9ff; }
.file-item:last-child { border-bottom: none; }
.file-info { flex: 1; min-width: 200px; }
.file-name { font-weight: 500; margin-right: 18px; }
.file-sizes { display: flex; gap: 12px; margin-top: 4px; }
.file-size { color: #666; font-size: 14px; }
.file-size.compressed { color: #67c23a; font-weight: 500; }
.file-ratio { font-size: 14px; font-weight: 500; }
.ratio-high { color: #67c23a; }
.ratio-medium { color: #e6a23c; }
.ratio-low { color: #909399; }
.file-actions { display: flex; gap: 8px; }
.file-status { display: flex; align-items: center; gap: 6px; font-size: 14px; }
.file-status.done { color: #67c23a; }
.file-status.error { color: #f56c6c; }
.file-status .is-loading { animation: rotate 1s linear infinite; }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
ul { padding-left: 20px; margin: 0; line-height: 1.7; }
.mt-2 { margin-top: 8px; }
.mb-4 { margin-bottom: 16px; }
</style>
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog

更多推荐文章

查看全部
  • 综合评价模型:层次 - 熵权 - 变异系数 - 博弈组合法 Python 实现
  • Windows 系统如何彻底卸载所有 pip 安装的包
  • Python Pandas Timestamp 常用方法及 DatetimeArray 类详解
  • Python ddgs 模块安装与实战:调用 DuckDuckGo 搜索 API
  • Linux 互斥锁原理与 C++ RAII 封装实战
  • C++ 继承进阶:多继承、菱形继承与虚继承机制
  • Windows 下编译 Open3D CUDA 版本指南及常见错误修复

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown 转 HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online

  • HTML 转 Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online

压缩效果对比

压缩质量图片质量滑块适用场景预期压缩比
高质量80%-100%存档、打印、重要文档20%-40%
中等质量50%-70%日常办公、邮件发送40%-60%
低质量10%-40%预览、快速传输60%-80%

典型使用场景

  • 邮件附件优化:将超过邮箱附件限制的 PDF 文件压缩到允许范围内,便捷发送。
  • 文档存储节省空间:批量压缩合同、报告等文档,节省云端或本地存储空间。
  • 网站上传速度提升:压缩需要在网站上上传的 PDF 文件,加快上传和加载速度。
  • 移动设备阅读优化:减小 PDF 体积,使其在手机、平板上更流畅地打开和浏览。

如何使用(三步完成压缩)

  1. 上传文件:点击'选择 PDF 文件'按钮,可批量选择多个需要压缩的 PDF 文件。
  2. 设置参数:
    • 选择压缩质量预设(高/中/低)
    • 调整图片质量滑块(10%-100%)
  3. 开始压缩:点击'开始压缩'按钮,等待处理完成。压缩后可单独下载或批量打包下载。

总结

这款开源、纯前端的 PDF 压缩工具,通过智能的图像优化和冗余数据移除技术,在保证文档可读性的前提下显著减小文件体积。它遵循对用户隐私的严格保护理念,所有处理均在本地完成,让您可以放心地压缩任何敏感文档。

无论您是需要频繁处理大型 PDF 的职场人士,还是希望优化网站加载速度的开发者,这款工具都能为您提供高效、安全、可控的压缩体验。

  • C++ 实现 AVL 平衡二叉搜索树详解
  • C++ CAS 原子操作详解与 ABA 问题解决方案
  • Qt/C++ 实现逻辑电路设计软件原理图绘制
  • JDK 17 新特性整理
  • FastJson2 完整使用指南(Java 后端企业级实战)
  • Linux 环境下 Git 版本控制工具使用指南
  • IPTV 播放源检测指南:故障排查与智能监测开源方案
  • 基于 GeoTools 和 SpringBoot 的省域驾车最快路线生成实践
  • WebGL 缓冲区使用与多点绘制实战
  • 数据结构:堆及堆的应用
  • SVD 奇异值分解原理、推导及应用实例
  • 双向最大匹配算法在古诗词与现代文分词中的应用效果
  • MPC 控制算法原理及流程