diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift index 4bb060377..0fbcac010 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -12,6 +12,9 @@ package import CoreGraphics package import ImageIO private import UniformTypeIdentifiers +#if canImport(UniformTypeIdentifiers_Private) +@_spi(Private) private import UniformTypeIdentifiers +#endif /// A protocol describing images that can be converted to instances of /// [`Attachment`](https://developer.apple.com/documentation/testing/attachment) @@ -47,6 +50,20 @@ package protocol AttachableAsCGImage: AttachableAsImage { var attachmentScaleFactor: CGFloat { get } } +/// All type identifiers supported by Image I/O. +@available(_uttypesAPI, *) +private let _supportedTypeIdentifiers = Set(CGImageDestinationCopyTypeIdentifiers() as? [String] ?? []) + +/// All content types supported by Image I/O. +@available(_uttypesAPI, *) +private let _supportedContentTypes = { +#if canImport(UniformTypeIdentifiers_Private) + UTType._types(identifiers: _supportedTypeIdentifiers).values +#else + _supportedTypeIdentifiers.compactMap(UTType.init(_:)) +#endif +}() + @available(_uttypesAPI, *) extension AttachableAsCGImage { package var attachmentOrientation: CGImagePropertyOrientation { @@ -63,8 +80,21 @@ extension AttachableAsCGImage { // Convert the image to a CGImage. let attachableCGImage = try attachableCGImage + // Determine the base content type to use. We do a naïve case-sensitive + // string comparison on the identifier first as it's faster than querying + // the corresponding UTType instances (because it doesn't need to touch the + // Launch Services database). The common cases where the developer passes + // no image format or passes .png/.jpeg are covered by the fast path. + var contentType = imageFormat.contentType + if !_supportedTypeIdentifiers.contains(contentType.identifier) { + guard let baseType = _supportedContentTypes.first(where: contentType.conforms(to:)) else { + throw ImageAttachmentError.unsupportedImageFormat(contentType.identifier) + } + contentType = baseType + } + // Create the image destination. - guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else { + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else { throw ImageAttachmentError.couldNotCreateImageDestination } diff --git a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift index e172abd8c..b1ad5a347 100644 --- a/Sources/Testing/Attachments/Images/ImageAttachmentError.swift +++ b/Sources/Testing/Attachments/Images/ImageAttachmentError.swift @@ -25,6 +25,9 @@ package enum ImageAttachmentError: Error { /// The image could not be converted. case couldNotConvertImage + + /// The specified content type is not supported by Image I/O. + case unsupportedImageFormat(_ typeIdentifier: String) #elseif os(Windows) /// A call to `QueryInterface()` failed. case queryInterfaceFailed(Any.Type, CLong) @@ -57,6 +60,8 @@ extension ImageAttachmentError: CustomStringConvertible { "Could not create the Core Graphics image destination to encode this image." case .couldNotConvertImage: "Could not convert the image to the specified format." + case let .unsupportedImageFormat(typeIdentifier): + "Could not convert the image to the format '\(typeIdentifier)' because the system does not support it." } #elseif os(Windows) switch self { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index b457fc271..623ead084 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -597,6 +597,33 @@ extension AttachmentTests { } } + @available(_uttypesAPI, *) + @Test func attachCGImageWithCustomUTType() throws { + let contentType = try #require(UTType(tag: "derived-from-jpeg", tagClass: .filenameExtension, conformingTo: .jpeg)) + let format = AttachableImageFormat(contentType: contentType) + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: format) + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in + #expect(buffer.count > 32) + } + if let ext = format.contentType.preferredFilenameExtension { + #expect(attachment.preferredName == ("diamond" as NSString).appendingPathExtension(ext)) + } + } + + @available(_uttypesAPI, *) + @Test func attachCGImageWithUnsupportedImageType() throws { + let contentType = try #require(UTType(tag: "unsupported-image-format", tagClass: .filenameExtension, conformingTo: .image)) + let format = AttachableImageFormat(contentType: contentType) + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: format) + #expect(attachment.attachableValue === image) + #expect(throws: ImageAttachmentError.self) { + try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in } + } + } + #if !SWT_NO_EXIT_TESTS @available(_uttypesAPI, *) @Test func cannotAttachCGImageWithNonImageType() async {