如何高效地将PDF转换为PNG?

5
我有以下函数将PDF文件转换为一系列图像(每页一个图像):
import Quartz

func convertPDF(at sourceURL: URL, to destinationURL: URL, fileType: NSBitmapImageFileType, dpi: CGFloat = 200) throws -> [URL] {
    let fileExtension: String
    switch fileType {
    case .BMP:              fileExtension = "bmp"
    case .GIF:              fileExtension = "gif"
    case .JPEG, .JPEG2000:  fileExtension = "jpeg"
    case .PNG:              fileExtension = "png"
    case .TIFF:             fileExtension = "tiff"
    }

    let data = try Data(contentsOf: sourceURL)
    let pdfImageRep = NSPDFImageRep(data: data)!
    var imageURLs = [URL]()

    for i in 0..<pdfImageRep.pageCount {
        pdfImageRep.currentPage = i

        let width = pdfImageRep.size.width / 72 * dpi
        let height = pdfImageRep.size.height / 72 * dpi
        let image = NSImage(size: CGSize(width: width, height: height), flipped: false) { dstRect in
            pdfImageRep.draw(in: dstRect)
        }

        let bitmapImageRep = NSBitmapImageRep(data: image.tiffRepresentation!)!
        let bitmapData = bitmapImageRep.representation(using: fileType, properties: [:])!

        let imageURL = destinationURL.appendingPathComponent("\(sourceURL.deletingPathExtension().lastPathComponent)-Page\(i+1).\(fileExtension)")
        try bitmapData.write(to: imageURL, options: [.atomic])
        imageURLs.append(imageURL)
    }

    return imageURLs
}

这个工作很好,性能不是非常快,但这并不是关键。我的问题与内存消耗有关。假设我正在转换一个长PDF(苹果的10-Q,51页长):

let sourceURL = URL(string: "http://files.shareholder.com/downloads/AAPL/4907179320x0x952191/4B5199AE-34E7-47D7-8502-CF30488B3B05/10-Q_Q3_2017_As-Filed_.pdf")!
let destinationURL = URL(fileURLWithPath: "/Users/mike/PDF")
let _ = try convertPDF(at: sourceURL, to: destinationURL, fileType: .PNG, dpi: 200)

最后一页时,内存使用量达到了约11GB!

我还注意到以下几点:

  • 当我通过Instruments运行它时,令人惊讶的是没有泄漏。两个大内存消耗者是bitmapImageRepbitmapData。它们在迭代之间似乎没有被释放。
  • 分析它会使性能变差,即使与Debug版本相比也是如此。
  • 降低DPI显然会减少内存占用,但行为仍然相同。内存随页面数量线性增加。
  • 无论是将51页PDF转换为单个PDF还是将51个单独的单页PDF转换为图像,情况都是一样的。

那么如何减少内存占用?有更好的将PDF转换为图像的方法吗?

1个回答

16

在经过一整天的挣扎后,我最终回答了自己的问题。

解决方案是降低到核心图形和图像I / O框架,将每个PDF页面呈现为位图上下文。这个问题非常适合并行化,因为可以将每个页面转换为一个单独的线程中的位图。

struct ImageFileType {
    var uti: CFString
    var fileExtention: String

    // This list can include anything returned by CGImageDestinationCopyTypeIdentifiers()
    // I'm including only the popular formats here
    static let bmp = ImageFileType(uti: kUTTypeBMP, fileExtention: "bmp")
    static let gif = ImageFileType(uti: kUTTypeGIF, fileExtention: "gif")
    static let jpg = ImageFileType(uti: kUTTypeJPEG, fileExtention: "jpg")
    static let png = ImageFileType(uti: kUTTypePNG, fileExtention: "png")
    static let tiff = ImageFileType(uti: kUTTypeTIFF, fileExtention: "tiff")
}

func convertPDF(at sourceURL: URL, to destinationURL: URL, fileType: ImageFileType, dpi: CGFloat = 200) throws -> [URL] {
    let pdfDocument = CGPDFDocument(sourceURL as CFURL)!
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.noneSkipLast.rawValue

    var urls = [URL](repeating: URL(fileURLWithPath : "/"), count: pdfDocument.numberOfPages)
    DispatchQueue.concurrentPerform(iterations: pdfDocument.numberOfPages) { i in
        // Page number starts at 1, not 0
        let pdfPage = pdfDocument.page(at: i + 1)!

        let mediaBoxRect = pdfPage.getBoxRect(.mediaBox)
        let scale = dpi / 72.0
        let width = Int(mediaBoxRect.width * scale)
        let height = Int(mediaBoxRect.height * scale)

        let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo)!
        context.interpolationQuality = .high
        context.setFillColor(.white)
        context.fill(CGRect(x: 0, y: 0, width: width, height: height))
        context.scaleBy(x: scale, y: scale)
        context.drawPDFPage(pdfPage)

        let image = context.makeImage()!
        let imageName = sourceURL.deletingPathExtension().lastPathComponent
        let imageURL = destinationURL.appendingPathComponent("\(imageName)-Page\(i+1).\(fileType.fileExtention)")

        let imageDestination = CGImageDestinationCreateWithURL(imageURL as CFURL, fileType.uti, 1, nil)!
        CGImageDestinationAddImage(imageDestination, image, nil)
        CGImageDestinationFinalize(imageDestination)

        urls[i] = imageURL
    }
    return urls
}

使用方法:

let sourceURL = URL(string: "http://files.shareholder.com/downloads/AAPL/4907179320x0x952191/4B5199AE-34E7-47D7-8502-CF30488B3B05/10-Q_Q3_2017_As-Filed_.pdf")!
let destinationURL = URL(fileURLWithPath: "/Users/mike/PDF")
let urls = try convertPDF(at: sourceURL, to: destinationURL, fileType: .png, dpi: 200)

现在转换速度非常快。内存使用量要低得多。显然,您所选择的DPI越高,需要的CPU和内存就越多。由于我只有一张弱劲的英特尔集成GPU,所以无法确定是否支持GPU加速。


我有一个问题 - 可能听起来有点愚蠢,我的API响应返回“image”字符串,它可能是PDF或PNG类型,还有另一个属性Type,它捕获类型“png”或“pdf”,我使用base64Encoded解码了该字符串,如何将此解码数据转换为PNG?有什么指针吗? - user2525211
我注意到,通过CoreGraphics API将PDF导出为JPEG时,导出的图像缺少在PDF页面上进行的注释(通过PencilKit)。有什么解决办法吗? - peetadelic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接