As part of our workflow we update exif data using the Core Image API. Specifically we get metadata from an original Image, take the metadata, modify the metadata and then use CGImageDestinationCopyImageSource() to merge the new metadata into a copy of the original image. We have found that if we compile to an iOS 15 device from XCode 13, the updated metadata is no longer merged in.
The following test code demonstrates the problem. (in order for it to work for you, you'll need to change the image input, and make sure to change an exif tag that exists in your image) The test will pass if the target device is below iOS 15, but fail if on iOS 15
func testCGImageDestinationCopyImageSource() throws {
guard let imageURL = Bundle(for: self.classForCoder).url(forResource: "Image_000001", withExtension: "jpg") else {
XCTFail()
return
}
// Work with the image data
let originalData = try Data(contentsOf: imageURL)
// Create source from data
guard let imageSource = CGImageSourceCreateWithData(originalData as CFData, nil) else {
XCTFail()
return
}
guard let UTI: CFString = CGImageSourceGetType(imageSource) else {
XCTFail()
return
}
// Setup a new destination to copy data too
let imageData: CFMutableData = CFDataCreateMutable(nil, 0)
guard let destination = CGImageDestinationCreateWithData(imageData as CFMutableData, UTI, 1, nil) else {
XCTFail()
return
}
// Get the metadata
var mutableMetadata: CGMutableImageMetadata
if let imageMetadata = CGImageSourceCopyMetadataAtIndex(imageSource, 0, nil) {
mutableMetadata = CGImageMetadataCreateMutableCopy(imageMetadata) ?? CGImageMetadataCreateMutable()
} else {
mutableMetadata = CGImageMetadataCreateMutable()
}
// Inspect and check the old value
guard let tag = CGImageMetadataCopyTagMatchingImageProperty(mutableMetadata,
kCGImagePropertyExifDictionary,
kCGImagePropertyExifLensModel) else {
XCTFail()
return
}
guard let originalValue = CGImageMetadataTagCopyValue(tag) as? String else {
XCTFail()
return
}
XCTAssertEqual(originalValue, "iOS.0")
// Set a new value in the metadata
CGImageMetadataSetValueMatchingImageProperty(mutableMetadata,
kCGImagePropertyExifDictionary,
kCGImagePropertyExifLensModel, "iOS" as CFString)
// Ensure new value is set in the metadata
guard let newTag = CGImageMetadataCopyTagMatchingImageProperty(mutableMetadata,
kCGImagePropertyExifDictionary,
kCGImagePropertyExifLensModel) else {
XCTFail()
return
}
guard let newValue = CGImageMetadataTagCopyValue(newTag) as? String else {
XCTFail()
return
}
XCTAssertEqual(newValue, "iOS")
// Combine the new metadata with the original image
let options = [
kCGImageDestinationMetadata as String : mutableMetadata,
kCGImageDestinationMergeMetadata as String : true
] as [String : Any]
guard CGImageDestinationCopyImageSource(destination, imageSource, options as CFDictionary, nil) else {
XCTFail()
return
}
// Create a new source from the mutated data
guard let newSource = CGImageSourceCreateWithData(imageData as CFData, nil) else {
XCTFail()
return
}
// Get a new copy of the metadata
var mutableMetadata2: CGMutableImageMetadata
if let imageMetadata2 = CGImageSourceCopyMetadataAtIndex(newSource, 0, nil) {
mutableMetadata2 = CGImageMetadataCreateMutableCopy(imageMetadata2) ?? CGImageMetadataCreateMutable()
} else {
mutableMetadata2 = CGImageMetadataCreateMutable()
}
// Inspect and check the changed value
guard let updatedTag = CGImageMetadataCopyTagMatchingImageProperty(mutableMetadata2,
kCGImagePropertyExifDictionary,
kCGImagePropertyExifLensModel) else {
XCTFail()
return
}
guard let updatedValue = CGImageMetadataTagCopyValue(updatedTag) as? String else {
XCTFail()
return
}
XCTAssertEqual(updatedValue, "iOS")
}