CGImageDestinationCopyImageSource no longer merges metadata on iOS 15

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")
  }

I submitted feedback : FB9660480 This is quite a serious issue for us, other frameworks to update metadata do not provide the facilities we need, and if this doesn't work, we'll have to redo our whole metadata editing pipeline with an external library

This test passes on iOS 15.1 so it seems it is fixed at that version The test still fails for iOS 15.0

I can confirm this issue still hasn't been fixed in either iOS 15.1 or iOS 15.2

I was having the same issue as the original poster. I realized that in my code, as well as in their code, I don't actually need to merge the metadata, as I've actually created an updated version using the mutable copy of the original metadata.

Once I removed the following line:

let options = [
      kCGImageDestinationMetadata as String : mutableMetadata,
      // kCGImageDestinationMergeMetadata as String : true   // <-- remove this line
      ] as [String : Any]

it works as expected. The full metadata in mutableMetadata is written properly. I've only tested on jpg and heic files.

I do agree that kCGImageDestinationMergeMetadata = true should work as expected, but this work-around is a decent fix.

The full code for my working example is here: https://gist.github.com/rob-secondstage/99d30f4e9804c5f250dcb063f779fd87

CGImageDestinationCopyImageSource no longer merges metadata on iOS 15
 
 
Q