照片扩展:无法保存未定向的图像 "Up"

Photos Extension: Can't Save Images Not Oriented "Up"

TL; DR:iOS Photo Editing Extension 无法保存对照片的更改,除非它们是使用 横向 方向的设备拍摄的。


我正在尝试在 iOS 上开发照片编辑扩展。

我的代码基于 Xcode 模板、Apple's sample code 和一些在线教程。

我注意到有些照片在应用更改后无法保存;我收到一个 警报视图 ,上面写着:

Unable to Save Changes

An error occurred while saving. Please try again later.

OK

在网上搜索后,我在 Stack Overflow 上遇到了以下两个问题:

  1. (已应用修复,在我的情况下不起作用)
  2. IOS) Photo Extension Unable To Save Changes Issue(没有有用的问题答案)

我尝试了几个编辑扩展来确保我的设备没有问题,并发现问题出现在:

  1. 我的应用,
  2. Apple自己的示例代码,
  3. AppStore 上的一些 第三方应用程序(例如,Litely),但不包括其他应用程序(例如,BitCam - 我希望我可以联系该应用程序的开发人员,询问一些提示...)。

我注意到,对于库中的给定照片资产,问题要么总是发生,要么永远不会发生。也就是说,它似乎取决于正在编辑的照片的某些 属性(因此在这种情况下整个 "Try again later" 业务毫无意义)。

我决定在finishContentEditing(completionHandler:)方法里面打断点(调用保存修改后的图片到框架指定的URL),查看[=13的各种属性=] 在编辑会话开始时传递的对象。

我很快意识到问题总是出现在 人像人像颠倒 中使用 iPhone 拍摄的照片],或 横向右 方向,并且仅此而已。在横向(主页按钮在右边)拍摄的照片可以毫无问题地保存。

Apple 的示例代码所做的是:

  1. PHContentEditingInput 实例的 fullSizeImageURL 属性 创建一个 CIIMage 实例。
  2. 通过调用 applyingOrientation() 从点 #1 创建图像的 定向副本,传递 fullSizeImageOrientation [=220= 的值] 的输入。
  3. 将适当的 CoreImage 过滤器应用于第 2 点的全尺寸定向图像。
  4. 创建 CIContext.
  5. 使用上下文调用writeJPEGRepresentation(of:to:colorSpace:)传递在#3中获得的修改后的CIImage,PHContentEditingOutputrenderedContentURL和原始[=的颜色space 23=].

实际代码:

DispatchQueue.global(qos: .userInitiated).async {
    // Load full-size image to process from input.
    guard let url = input.fullSizeImageURL
        else { fatalError("missing input image url") }
    guard let inputImage = CIImage(contentsOf: url)
        else { fatalError("can't load input image to apply edit") }

    // Define output image with Core Image edits.
    let orientedImage = inputImage//.applyingOrientation(input.fullSizeImageOrientation)
    let outputImage: CIImage
    switch selectedFilterName {
        case .some(wwdcFilter):
            outputImage = orientedImage.applyingWWDCDemoEffect()
        case .some(let filterName):
            outputImage = orientedImage.applyingFilter(filterName, parameters: [:])
        default:
            outputImage = orientedImage
    }

    // Usually you want to create a CIContext early and reuse it, but
    // this extension uses one (explicitly) only on exit.
    let context = CIContext()
    // Render the filtered image to the expected output URL.
    if #available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) {
        // Use Core Image convenience method to write JPEG where supported.
        do {
            try context.writeJPEGRepresentation(of: outputImage, to: output.renderedContentURL, colorSpace: inputImage.colorSpace!)
            completionHandler(output)
        } catch let error {
            NSLog("can't write image: \(error)")
            completionHandler(nil)
        }
    } else {
        // Use CGImageDestination to write JPEG in older OS.
        guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent)
            else { fatalError("can't create CGImage") }
        guard let destination = CGImageDestinationCreateWithURL(output.renderedContentURL as CFURL, kUTTypeJPEG, 1, nil)
            else { fatalError("can't create CGImageDestination") }
        CGImageDestinationAddImage(destination, cgImage, nil)
        let success = CGImageDestinationFinalize(destination)
        if success {
            completionHandler(output)
        } else {
            completionHandler(nil)
        }
    }
}

(这里稍微重构为 post。上面代码的 bluk 驻留在一个 separate 方法中,从调度队列块中调用)


当我尝试编辑使用设备拍摄的照片时(例如)横向右方向:

...选择 Apple 的示例代码照片编辑扩展:

...应用 "Sepia" 过滤器并点击 "Done":

...我收到可怕的警报:

...在关闭它之后,图像预览以某种方式旋转到相对于横向左侧的方向:

(即,在横向中拍摄的照片旋转了180度,在人像中拍摄的照片旋转了90度等)

点击 "Done" 或 "Cancel" 然后 "Discard Changes" 完成会话,图像恢复到正确的方向:

显然,有一些我和 Litely 的开发人员以及 Apple 的 2016 示例代码都不知道的陷阱(但是 BitCam 的开发人员 是)。

这是怎么回事?


解决方法?

如果我用 iPhone 在 纵向 方向拍照并尝试编辑它,在调试器上 fullSizeImageOrientation.right 并且如上所述编辑失败。

但是如果我使用默认工具将图像旋转一次 180 度:

...保存,再次编辑并旋转 另一个 180 度(或者,90 + 270 度,但总是在 两次单独的编辑中), 将其恢复到 原始方向 ,然后 然后 尝试使用扩展进行编辑,现在fullSizeImageOrientation的值为.up保存成功。我相信这是因为这个工具实际上旋转了 像素数据 而不是仅仅修改方向 元数据 (事实上它可以在 任意角度,不只是 90 度的倍数,我认为会泄露...)

当然,这将需要不便的用户交互,因此它并不是真正的解决方法(不过,编程等效项是)。


附录:

我使用的是Xcode 10.0,以上在iPhone 8 运行 iOS 12 GM和iPhone 5s上均已确认运行 iOS 11.4.1).

我当然看到保存失败是因为方向错误,但以下架构目前似乎适合我:

func startContentEditing(with contentEditingInput: PHContentEditingInput, placeholderImage: UIImage) {
    self.input = contentEditingInput
    if let im = self.input?.displaySizeImage {
        self.displayImage = CIImage(image:im, options: [.applyOrientationProperty:true])!
        // ... other stuff depending on what the adjustment data was ...
    }
    self.mtkview.setNeedsDisplay()
}
func finishContentEditing(completionHandler: @escaping ((PHContentEditingOutput?) -> Void)) {
    DispatchQueue.global(qos:.default).async {
        let inurl = self.input!.fullSizeImageURL!
        let output = PHContentEditingOutput(contentEditingInput:self.input!)
        let outurl = output.renderedContentURL
        var ci = CIImage(contentsOf: inurl, options: [.applyOrientationProperty:true])!
        let space = ci.colorSpace!
        // ... apply real filter to `ci` based on user edits ...
        try! CIContext().writeJPEGRepresentation(
            of: ci, to: outurl, colorSpace: space)
        let data = // whatever
        output.adjustmentData = PHAdjustmentData(
            formatIdentifier: self.myidentifier, formatVersion: "1.0", data: data)
        completionHandler(output)
    }
}