macOS 12 beta | Updating metadata silently fails at file level

I use ImageI/O call - CGImageDestinationCopyImageSource() to update image metadata and noticed it's failing on updating existing metadata fields since macOS 12 beta 5, and upgrading to beta 6 is not fixing it. MacOS 11 and below don't have this issue. Do you plan to fix it in coming betas and when?

Precondition:

  • have a file that has any IPTC. For example, IPTC Title = "Old".

Steps:

Update the metadata (e.g. change IPTC Title to "New") with following code:

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:(id)metadataToApply,kCGImageDestinationMetadata, kCFBooleanTrue,kCGImageDestinationMergeMetadata, nil]; CFErrorRef metaError = NULL;

BOOL isSuccess = CGImageDestinationCopyImageSource(idst, srf, (__bridge CFDictionaryRef)options, &metaError);

Observe: At file level, silently fails, IPTC Title = "Old".

Replies

I think I'm also running into this problem but on iOS 15. I use CGImageDestinationCopyImageSource to update some metadata. I ran an XCode test to make sure it was correctly set, but since I updated to XCode 13 and iOS 15, the test fails and the metadata is not updated in the file

That shouldn't have been an answer, missed the comment button.

Anyway, I have replicated the issue in a single test. For me the issue manifests itself in iOS 15, if I run the test on a device running ios 14, the test passes.

Here is the full code for a test, you'll need to supply your own image and change a tag that exists for you:

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 copied to data
    guard let newSource = CGImageSourceCreateWithData(imageData as CFData, nil) else {
      XCTFail()
      return
    }
    // Get the metadata from the copied to data
    var mutableMetadata2: CGMutableImageMetadata
    if let imageMetadata2 = CGImageSourceCopyMetadataAtIndex(newSource, 0, nil) {
      mutableMetadata2 = CGImageMetadataCreateMutableCopy(imageMetadata2) ?? CGImageMetadataCreateMutable()
    } else {
      mutableMetadata2 = CGImageMetadataCreateMutable()
    }
    // Inspect and check the value in the copied to data
    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 a bug to Apple, this is quite a serious issue for us.

  • There is a horrid workaround. The kCGImageDestinationMergeMetadata set to true is broke. It seems new metadata tags can be added but not modified or even removed. If you call CGImageDestinationCopyImageSource with an options dictionary containing just an empty CGImageMetadata then all metadata is removed. From the resulting image data, create another source image and destination image and call CGImageDestinationCopyImageSource a second time but with the required metadata.

Add a Comment