Skip to content

Commit 6a52f30

Browse files
authored
Handle image types derived from the base types. (#1399)
On Apple platforms, you can declare file types (uniform type identifiers, or "UTTypes" for short) that conform to existing types. That means a developer could conceivably say "save this image as .florb" where `.florb` represents a type that conforms to, but is not identical to, `.jpeg`. This PR ensures that if a developer passes such a type, we handle it by passing the supported base type to Image I/O. And, if the developer passes some image format that is unsupported by Image I/O, we can throw an error with a more specific diagnostic than just "it failed." ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 021ff08 commit 6a52f30

File tree

3 files changed

+63
-1
lines changed

3 files changed

+63
-1
lines changed

Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
package import CoreGraphics
1313
package import ImageIO
1414
private import UniformTypeIdentifiers
15+
#if canImport(UniformTypeIdentifiers_Private)
16+
@_spi(Private) private import UniformTypeIdentifiers
17+
#endif
1518

1619
/// A protocol describing images that can be converted to instances of
1720
/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment)
@@ -47,6 +50,20 @@ package protocol AttachableAsCGImage: AttachableAsImage {
4750
var attachmentScaleFactor: CGFloat { get }
4851
}
4952

53+
/// All type identifiers supported by Image I/O.
54+
@available(_uttypesAPI, *)
55+
private let _supportedTypeIdentifiers = Set(CGImageDestinationCopyTypeIdentifiers() as? [String] ?? [])
56+
57+
/// All content types supported by Image I/O.
58+
@available(_uttypesAPI, *)
59+
private let _supportedContentTypes = {
60+
#if canImport(UniformTypeIdentifiers_Private)
61+
UTType._types(identifiers: _supportedTypeIdentifiers).values
62+
#else
63+
_supportedTypeIdentifiers.compactMap(UTType.init(_:))
64+
#endif
65+
}()
66+
5067
@available(_uttypesAPI, *)
5168
extension AttachableAsCGImage {
5269
package var attachmentOrientation: CGImagePropertyOrientation {
@@ -63,8 +80,21 @@ extension AttachableAsCGImage {
6380
// Convert the image to a CGImage.
6481
let attachableCGImage = try attachableCGImage
6582

83+
// Determine the base content type to use. We do a naïve case-sensitive
84+
// string comparison on the identifier first as it's faster than querying
85+
// the corresponding UTType instances (because it doesn't need to touch the
86+
// Launch Services database). The common cases where the developer passes
87+
// no image format or passes .png/.jpeg are covered by the fast path.
88+
var contentType = imageFormat.contentType
89+
if !_supportedTypeIdentifiers.contains(contentType.identifier) {
90+
guard let baseType = _supportedContentTypes.first(where: contentType.conforms(to:)) else {
91+
throw ImageAttachmentError.unsupportedImageFormat(contentType.identifier)
92+
}
93+
contentType = baseType
94+
}
95+
6696
// Create the image destination.
67-
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else {
97+
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else {
6898
throw ImageAttachmentError.couldNotCreateImageDestination
6999
}
70100

Sources/Testing/Attachments/Images/ImageAttachmentError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ package enum ImageAttachmentError: Error {
2525

2626
/// The image could not be converted.
2727
case couldNotConvertImage
28+
29+
/// The specified content type is not supported by Image I/O.
30+
case unsupportedImageFormat(_ typeIdentifier: String)
2831
#elseif os(Windows)
2932
/// A call to `QueryInterface()` failed.
3033
case queryInterfaceFailed(Any.Type, CLong)
@@ -57,6 +60,8 @@ extension ImageAttachmentError: CustomStringConvertible {
5760
"Could not create the Core Graphics image destination to encode this image."
5861
case .couldNotConvertImage:
5962
"Could not convert the image to the specified format."
63+
case let .unsupportedImageFormat(typeIdentifier):
64+
"Could not convert the image to the format '\(typeIdentifier)' because the system does not support it."
6065
}
6166
#elseif os(Windows)
6267
switch self {

Tests/TestingTests/AttachmentTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,33 @@ extension AttachmentTests {
597597
}
598598
}
599599

600+
@available(_uttypesAPI, *)
601+
@Test func attachCGImageWithCustomUTType() throws {
602+
let contentType = try #require(UTType(tag: "derived-from-jpeg", tagClass: .filenameExtension, conformingTo: .jpeg))
603+
let format = AttachableImageFormat(contentType: contentType)
604+
let image = try Self.cgImage.get()
605+
let attachment = Attachment(image, named: "diamond", as: format)
606+
#expect(attachment.attachableValue === image)
607+
try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in
608+
#expect(buffer.count > 32)
609+
}
610+
if let ext = format.contentType.preferredFilenameExtension {
611+
#expect(attachment.preferredName == ("diamond" as NSString).appendingPathExtension(ext))
612+
}
613+
}
614+
615+
@available(_uttypesAPI, *)
616+
@Test func attachCGImageWithUnsupportedImageType() throws {
617+
let contentType = try #require(UTType(tag: "unsupported-image-format", tagClass: .filenameExtension, conformingTo: .image))
618+
let format = AttachableImageFormat(contentType: contentType)
619+
let image = try Self.cgImage.get()
620+
let attachment = Attachment(image, named: "diamond", as: format)
621+
#expect(attachment.attachableValue === image)
622+
#expect(throws: ImageAttachmentError.self) {
623+
try attachment.attachableValue.withUnsafeBytes(for: attachment) { _ in }
624+
}
625+
}
626+
600627
#if !SWT_NO_EXIT_TESTS
601628
@available(_uttypesAPI, *)
602629
@Test func cannotAttachCGImageWithNonImageType() async {

0 commit comments

Comments
 (0)