乐于分享
好东西不私藏

Zipic实录 6 -PDF压缩:利用macOS原生能力

Zipic实录 6 -PDF压缩:利用macOS原生能力

这是「Zipic 实录」系列第六篇。上一篇聊了文件夹监控自动压缩的实现。这一篇聊另一个核心功能——PDF 压缩,和图片压缩完全不同的技术路径。

PDF 压缩:利用 macOS 原生能力

PDF 作为日常文档中最常见的格式,很多时候里面就是塞满了图片。支持 PDF 压缩是用户呼声很高的需求。

PDF 压缩和普通图片压缩完全是两码事。图片压缩相对简单——读取像素数据,用算法重新编码,写入新文件。但 PDF 是复杂的容器格式,里面可能包含文本、矢量图形、嵌入字体、书签、表单……当然还有图片。关键问题是:如何在不破坏 PDF 结构的前提下,只对其中的图片进行压缩?

最初考虑过几种方案:Ghostscript 功能强大但体积也大,需要处理外部依赖的分发问题;ImageMagick 对 PDF 结构的保留不够好;自己解析 PDF 格式手动提取和重压缩图片——这条路想想就知道是个无底洞。

最终选择了 macOS 原生的 Quartz Filter 技术,说实话有点相见恨晚。它就藏在系统里专门干这个事,系统自带的「预览」应用导出 PDF 时的「减小文件大小」选项用的就是这个。工作原理很优雅:在渲染 PDF 页面时自动对其中的位图图像进行 JPEG 重压缩,而文本、矢量图形、字体这些保持原样不动。

这是 macOS 平台独有的优势——Core Graphics 框架有硬件加速支持,处理速度很快,而且不需要打包任何外部依赖,应用体积不会膨胀。

核心思路是动态生成自定义的 .qfilter 配置文件定义压缩参数,然后通过 Core Graphics 框架应用这个滤镜来渲染 PDF。整个流程:根据用户选择的压缩等级生成对应的 Quartz Filter 配置文件 → 读取源 PDF → 创建新的 PDF 上下文并应用 Filter → 逐页渲染(这一步 Filter 会自动处理图像压缩)→ 输出压缩后的 PDF。

关键代码(Quartz Filter 文件生成):

private static func generateQFilter(at url: URL, compressionQuality: Double, level: Int) {
    let
 xmlContent = """
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
    <plist version="1.0">
    <dict>
        <key>FilterData</key>
        <dict>
            <key>ColorSettings</key>
            <dict>
                <key>ImageSettings</key>
                <dict>
                    <key>Compression Quality</key>
                    <real>\(compressionQuality)</real>      <!-- 0.0-1.0 -->
                    <key>ImageCompression</key>
                    <string>ImageJPEGCompress</string>       <!-- JPEG 压缩 -->
                    <key>ImageScaleSettings</key>
                    <dict>
                        <key>ImageScaleFactor</key>
                        <real>1.0</real>                     <!-- 保持原尺寸 -->
                    </dict>
                </dict>
            </dict>
        </dict>
        <key>FilterType</key>
        <integer>1</integer>
        <key>Name</key>
        <string>Compress PDF</string>
    </dict>
    </plist>
    """

    try
 xmlContent.write(to: url, atomically: true, encoding: .utf8)
}

关键代码(PDF 压缩核心逻辑):

static func pdf(at sourceURL: URL, to destinationURL: URL, compressionLevel: Double) -> CommandResult {
    // 1. 加载 Quartz Filter

    guard
 let filter = QuartzFilterManager.filterForCompressionLevel(compressionLevel) else {
        return
 CommandResult(output: "Failed to load filter", error: .exit, status: -1)
    }
    // 2. 读取源 PDF

    guard
 let sourcePDF = PDFDocument(url: sourceURL) else {
        return
 CommandResult(output: "Failed to load PDF", error: .exit, status: -1)
    }
    // 3. 创建数据容器和 PDF 上下文

    let
 mutableData = NSMutableData()
    guard
 let consumer = CGDataConsumer(data: mutableData),
          let
 firstPage = sourcePDF.page(at: 0) else { return /* error */ }
    var
 mediaBox = firstPage.bounds(for: .mediaBox)
    guard
 let pdfContext = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
        return
 /* error */
    }
    // 4. 应用 Filter 到上下文(关键步骤)

    guard
 filter.apply(to: pdfContext) else {
        return
 CommandResult(output: "Failed to apply filter", error: .exit, status: -1)
    }
    // 5. 逐页渲染(Filter 自动处理图像压缩)

    for
 pageIndex in 0..<sourcePDF.pageCount {
        guard
 let page = sourcePDF.page(at: pageIndex) else { continue }
        var
 pageRect = page.bounds(for: .mediaBox)
        pdfContext.beginPage(mediaBox: &pageRect)
        page.draw(with: .mediaBox, to: pdfContext)
        pdfContext.endPage()
    }
    pdfContext.closePDF()
    try
 mutableData.write(to: destinationURL, options: .atomic)
    return
 CommandResult(output: "Success", error: .exit, status: 0)
}

压缩等级的设计上,我定义了 6 个级别,对应不同的 JPEG 质量参数(0.9 到 0.2)。值得注意的是,JPEG 压缩到 0.2 这个级别图片会出现明显的压缩痕迹,对于需要打印或展示的文档建议用较高的质量级别,如果只是用于网络传输或归档可以激进一些。

踩坑经验:动态生成的 .qfilter 文件需要存放在合适的位置(我选择了 ~/Library/zipic/filters/),要处理好文件不存在或损坏的情况,必要时重新生成。

另外要注意的是,Quartz Filter 只压缩 PDF 中的位图图像,如果一个 PDF 主要是文字和矢量图形,压缩后体积可能变化不大——这一点需要在产品层面向用户说明,避免「为什么压了半天没变小」的困惑。好消息是 PDF 中的元数据(书签、链接、表单等)在这个方案下都能完整保留,这是很多第三方方案做不到的。


下一篇聊聊缩略图生成的内存优化和设备指纹稳定性——一个是性能问题,一个是用户体验问题,都是实际运营中暴露出来的。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Zipic实录 6 -PDF压缩:利用macOS原生能力

评论 抢沙发

2 + 6 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮