Swift。如何从 HEIC 图像中提取单个子图像

Swift. How to extract the individual sub images from an HEIC image

我需要能够加载 heic 图像并提取所有子图像并将其输出为 png,类似于预览的方式。例如,如果您在预览中打开动态 heic 壁纸,它会在边栏中显示所有图像及其名称:

你是怎么做到的?我试过像下面这样使用 NSImage 。但这只输出一张图片:

let image = NSImage(byReferencing: url)
image.writePNG(toURL: newUrl)

您需要加载 HEIC 数据,获取其 CGImageSource 及其计数。然后创建一个从 0 到 count-1 的循环,并在相应的索引处获取每个图像。您可以在内存中使用这些 CGImages 创建一个数组或将它们写入磁盘(首选)。请注意,由于 HEIC 文件的大小为 186MB,这将需要一段时间才能执行。提取的每个图像将从 19MB 到 28MB。

func extractHeicImages(from url: URL) throws {
    let data = try Data(contentsOf: url)
    let location = url.deletingLastPathComponent()
    let pathExtension = url.pathExtension
    let fileName = url.deletingPathExtension().lastPathComponent
    let destinationFolder = location.appendingPathComponent(fileName)
    guard pathExtension == "heic", let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return }
    let count = CGImageSourceGetCount(imageSource)
    try FileManager.default.createDirectory(at: destinationFolder, withIntermediateDirectories: false, attributes: nil)
    for index in 0..<count {
        try autoreleasepool {
            if let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, nil) {
                let number = String(format: "#%05d", index)
                let destinationURL = destinationFolder
                    .appendingPathComponent(fileName+number)
                    .appendingPathExtension(pathExtension)
                try NSImage(cgImage: cgImage, size: .init(width: cgImage.width, height: cgImage.height))
                    .heic?
                    .write(to: destinationURL)
                print("saved image " + number)
            }
        }
    }
}

您还需要这些助手来从图像中提取 cgimate 并从中获取 HEIC 数据表示:

extension NSImage {
    var heic: Data? { heic() }
    var cgImage: CGImage? {
        var rect = NSRect(origin: .zero, size: size)
        
        return cgImage(forProposedRect: &rect, context: .current, hints: nil)
    }
    func heic(compressionQuality: CGFloat = 1) -> Data? {
        guard
            let mutableData = CFDataCreateMutable(nil, 0),
            let destination = CGImageDestinationCreateWithData(mutableData, "public.heic" as CFString, 1, nil),
            let cgImage = cgImage
        else { return nil }
        CGImageDestinationAddImage(destination, cgImage, [kCGImageDestinationLossyCompressionQuality: compressionQuality] as CFDictionary)
        guard CGImageDestinationFinalize(destination) else { return nil }
        return mutableData as Data
    }
}

游乐场测试。这假设“Catalina.heic”位于您的桌面。

let catalinaURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("Catalina.heic")
do {
    try extractHeicImages(from: catalinaURL)
} catch {
    print(error)
}

每个子图像由 NSBitmapImageRep 表示。循环图像代表,转换为 png 并保存:

let imageReps = image.representations
for imageIndex in 0..<imageReps.count {
    if let imageRep = imageReps[imageIndex] as? NSBitmapImageRep {
        if let data = imageRep.representation(using: .png, properties: [:]) {
            do {
                let url = folderURL.appendingPathComponent("image \(imageIndex).png", isDirectory: false)
                try data.write(to: url, options:[])
            } catch {
                print("Unexpected error: \(error).")
            }
        }
    }
}

转换为 png 需要一些时间。 运行 并行转换更快,但我不确定它是否保存:

DispatchQueue.concurrentPerform(iterations: imageReps.count) { iteration in
    if let imageRep = imageReps[iteration] as? NSBitmapImageRep {
        if let data = imageRep.representation(using: .png, properties: [:]) {
            do {
                let url = folderURL.appendingPathComponent("image \(iteration).png", isDirectory: false)
                try data.write(to: url, options:[])
            } catch {
                print("Unexpected error: \(error).")
            }
        }
    }
}