欢迎光临
我们一直在努力

PDF压缩工具:纯前端、开源、高效智能的本地压缩方案(在线demo+实现思路)

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

为此,吾爱IIS(52IIS.COM) 推出了这款完全开源的 PDF压缩工具。它采用先进的纯前端技术,通过智能优化图片质量和移除冗余数据,在保证可读性的前提下显著减小PDF文件体积。所有处理均在浏览器本地完成,确保您的文档隐私零泄露。作为开源项目,您可以在 GitHub 上查看其完整源代码。

✨ 核心功能亮点
  • 智能压缩,灵活可控

    • 多级压缩质量:提供高质量(较小压缩)、中等质量(推荐)和低质量(最大压缩)三种预设,满足不同场景需求。

    • 精细图片质量控制:通过直观的滑块(10%-100%),可精确控制PDF中图片的压缩程度,在文件大小和视觉效果间找到最佳平衡。

  • 批量处理,高效便捷

    • 支持同时选择多个PDF文件进行压缩,大幅提升处理效率。

    • 压缩完成后,可单独下载每个文件,或一键打包下载所有压缩后的文件。

  • 实时反馈,清晰透明

    • 每个文件都显示原始大小、压缩后大小和压缩比例。

    • 压缩比例以颜色标识(高绿、中黄、低灰),直观呈现压缩效果。

    • 处理状态(等待中、压缩中、完成、失败)一目了然。

  • 隐私安全,纯前端处理

    • 所有压缩操作均在您的浏览器本地完成,PDF文件数据全程不会上传至任何服务器。

    • 基于 pdf.js 和 pdf-lib 两大专业库实现,确保处理过程的准确性和可靠性。

  • 广泛兼容,智能处理

    • 支持大部分标准PDF格式,通过智能渲染和重编码技术实现有效压缩。

    • 注:加密或损坏的PDF可能无法正常处理,系统会给出明确提示。

  • 💻 完整Vue 3 + TypeScript代码实现

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

    <template>
    <div class="flex flex-col flex-1 mt-3">
    <DetailHeader :title="info.title"></DetailHeader>

    <div class="p-4 bg-white rounded-2xl">
    <div class="mb-3">
    <el-text type="info">压缩PDF文档大小,支持批量压缩,所有操作在本地完成。</el-text>
    </div>
    <div class="mb-3">
    <el-text type="info">通过降低图片质量和移除冗余数据来减小PDF文件体积。</el-text>
    </div>

    <div class="mb-4 settings-section">
    <div class="settings-row">
    <div class="setting-item">
    <span class="setting-label">压缩质量:</span>
    <el-select v-model="compressQuality" style="width: 140px">
    <el-option label="高质量 (较小压缩)" value="high" />
    <el-option label="中等质量 (推荐)" value="medium" />
    <el-option label="低质量 (最大压缩)" value="low" />
    </el-select>
    </div>
    <div class="setting-item">
    <span class="setting-label">图片质量:</span>
    <el-slider v-model="imageQuality" :min="10" :max="100" :format-tooltip="(val: number) => `${val}%`" style="width: 200px" />
    </div>
    </div>
    </div>

    <div class="mb-3">
    <el-upload
    action=""
    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" class="mt-3">
    <div class="mb-3">
    <el-button type="success" @click="compressAllFiles" :disabled="loading">开始压缩</el-button>
    <el-button type="warning" @click="clearFiles" :disabled="loading">清空文件</el-button>
    </div>
    <div class="file-list">
    <div v-for="(file, idx) in files" :key="file.uid" class="file-item">
    <div class="file-info">
    <span class="file-name">{{ file.name }}</span>
    <div class="file-sizes">
    <span class="file-size">原始: {{ formatFileSize(file.size) }}</span>
    <span v-if="file.compressedSize" class="file-size compressed">
    压缩后: {{ formatFileSize(file.compressedSize) }}
    </span>
    <span v-if="file.compressedSize" class="file-ratio" :class="getRatioClass(file)">
    ({{ getRatio(file) }})
    </span>
    </div>
    </div>
    <div class="file-actions">
    <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'" class="file-status">
    <el-icon class="is-loading"><Loading /></el-icon>
    <span>压缩中…</span>
    </div>
    <div v-else-if="file.status === 'done'" class="file-status done">
    <el-icon><CircleCheck /></el-icon>
    <span>完成</span>
    </div>
    <div v-else-if="file.status === 'error'" class="file-status error">
    <el-icon><CircleClose /></el-icon>
    <span>失败</span>
    </div>
    </div>
    </div>
    </div>

    <div v-if="loading" class="mt-3">
    <el-alert type="info" :title="loadingMsg" show-icon :closable="false" />
    <el-progress :percentage="progress" :format="() => `${progress}%`" class="mt-2" />
    </div>

    <div v-if="errorMsg" class="mt-3">
    <el-alert type="error" :title="errorMsg" show-icon :closable="false" />
    </div>

    <div v-if="completedCount > 0" class="mt-3">
    <el-alert type="success" :title="`成功压缩 ${completedCount} 个文件`" show-icon :closable="false" />
    <div class="mt-3">
    <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>

    📊 压缩效果对比
    压缩质量图片质量滑块适用场景预期压缩比
    高质量 80%-100% 存档、打印、重要文档 20%-40%
    中等质量 50%-70% 日常办公、邮件发送 40%-60%
    低质量 10%-40% 预览、快速传输 60%-80%
    📝 典型使用场景
    • 邮件附件优化:将超过邮箱附件限制的PDF文件压缩到允许范围内,便捷发送。

    • 文档存储节省空间:批量压缩合同、报告等文档,节省云端或本地存储空间。

    • 网站上传速度提升:压缩需要在网站上上传的PDF文件,加快上传和加载速度。

    • 移动设备阅读优化:减小PDF体积,使其在手机、平板上更流畅地打开和浏览。

    🚀 如何使用(三步完成压缩)
  • 上传文件:点击“选择PDF文件”按钮,可批量选择多个需要压缩的PDF文件。

  • 设置参数:

    • 选择压缩质量预设(高/中/低)

    • 调整图片质量滑块(10%-100%)

  • 开始压缩:点击“开始压缩”按钮,等待处理完成。压缩后可单独下载或批量打包下载。

  • 💎 总结

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

    无论您是需要频繁处理大型PDF的职场人士,还是希望优化网站加载速度的开发者,这款工具都能为您提供高效、安全、可控的压缩体验。欢迎访问 吾爱IIS工具页 免费使用,也欢迎前往其 GitHub仓库 了解更多技术细节或参与贡献。

    赞(0)
    未经允许不得转载:171主机测评 » PDF压缩工具:纯前端、开源、高效智能的本地压缩方案(在线demo+实现思路)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址