From 0e289e0f38fd0c35319a22ad75f045f8d6d15caa Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 19 Sep 2024 14:25:02 -0400 Subject: [PATCH 1/3] Add a CoreGraphics cross-import overlay with support for attaching `CGImage`s. This PR adds a new cross-import overlay target with Apple's Core Graphics framework that allows attaching a `CGImage` as an attachment in an arbitrary image format (PNG, JPEG, etc.) Because `CGImage` is imported into Swift as a non-final class, it cannot conform directly to `Attachable`, so an `AttachableContainer` type acts as a proxy. This type is not meant to be used directly, so its name is underscored. Initializers on `Attachment` are provided so that this abstraction is almost entirely transparent to test authors. A new protocol, `AttachableAsCGImage`, is introduced that abstracts away the relationship between the attached image and Core Graphics; in the future, I intend to make additional image types like `NSImage and `UIImage` conform to this protocol too. The code in this PR is, by definition, specific to Apple's platforms. In the future, I'd be interested in adding Windows/Linux equivalents (`HBITMAP`? Whatever Gnome/KDE/Qt use?) but that's beyond the scope of this PR. --- Package.swift | 9 + .../Attachments/AttachableAsCGImage.swift | 89 ++++++++++ .../Attachment+AttachableAsCGImage.swift | 147 ++++++++++++++++ .../CGImage+AttachableAsCGImage.swift | 20 +++ .../Attachments/ImageAttachmentError.swift | 39 +++++ .../_AttachableImageContainer.swift | 163 ++++++++++++++++++ .../ReexportTesting.swift | 11 ++ Tests/TestingTests/AttachmentTests.swift | 88 ++++++++++ 8 files changed, 566 insertions(+) create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift create mode 100644 Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift create mode 100644 Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift diff --git a/Package.swift b/Package.swift index f840a21cb..b109c0447 100644 --- a/Package.swift +++ b/Package.swift @@ -51,6 +51,7 @@ let package = Package( name: "TestingTests", dependencies: [ "Testing", + "_Testing_CoreGraphics", "_Testing_Foundation", ], swiftSettings: .packageSettings @@ -91,6 +92,14 @@ let package = Package( ), // Cross-import overlays (not supported by Swift Package Manager) + .target( + name: "_Testing_CoreGraphics", + dependencies: [ + "Testing", + ], + path: "Sources/Overlays/_Testing_CoreGraphics", + swiftSettings: .packageSettings + ), .target( name: "_Testing_Foundation", dependencies: [ diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift new file mode 100644 index 000000000..14df843c6 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift @@ -0,0 +1,89 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +public import CoreGraphics +private import ImageIO + +/// A protocol describing images that can be converted to instances of +/// ``Testing/Attachment``. +/// +/// Instances of types conforming to this protocol do not themselves conform to +/// ``Testing/Attachable``. Instead, the testing library provides additional +/// initializers on ``Testing/Attachment`` that take instances of such types and +/// handle converting them to image data when needed. +/// +/// The following system-provided image types conform to this protocol and can +/// be attached to a test: +/// +/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +/// +/// You do not generally need to add your own conformances to this protocol. If +/// you have an image in another format that needs to be attached to a test, +/// first convert it to an instance of one of the types above. +@_spi(Experimental) +public protocol AttachableAsCGImage { + /// An instance of `CGImage` representing this image. + /// + /// - Throws: Any error that prevents the creation of an image. + var attachableCGImage: CGImage { get throws } + + /// The orientation of the image. + /// + /// The value of this property is the raw value of an instance of + /// `CGImagePropertyOrientation`. The default value of this property is + /// `.up`. + /// + /// This property is not part of the public interface of the testing + /// library. It may be removed in a future update. + var _attachmentOrientation: UInt32 { get } + + /// The scale factor of the image. + /// + /// The value of this property is typically greater than `1.0` when an image + /// originates from a Retina Display screenshot or similar. The default value + /// of this property is `1.0`. + /// + /// This property is not part of the public interface of the testing + /// library. It may be removed in a future update. + var _attachmentScaleFactor: CGFloat { get } + + /// Make a copy of this instance to pass to an attachment. + /// + /// - Returns: A copy of `self`, or `self` if no copy is needed. + /// + /// Several system image types do not conform to `Sendable`; use this + /// function to make copies of such images that will not be shared outside + /// of an attachment and so can be generally safely stored. + /// + /// The default implementation of this function when `Self` conforms to + /// `Sendable` simply returns `self`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _makeCopyForAttachment() -> Self +} + +extension AttachableAsCGImage { + public var _attachmentOrientation: UInt32 { + CGImagePropertyOrientation.up.rawValue + } + + public var _attachmentScaleFactor: CGFloat { + 1.0 + } +} + +extension AttachableAsCGImage where Self: Sendable { + public func _makeCopyForAttachment() -> Self { + self + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift new file mode 100644 index 000000000..bb963646b --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -0,0 +1,147 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +@_spi(ForSwiftTestingOnly) @_spi(Experimental) public import Testing + +public import UniformTypeIdentifiers + +extension Attachment { + /// Initialize an instance of this type that encloses the given image. + /// + /// - Parameters: + /// - attachableValue: The value that will be attached to the output of + /// the test run. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the testing library attempts + /// to derive a reasonable filename for the attached value. + /// - contentType: The image format with which to encode `attachableValue`. + /// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// the result is undefined. Pass `nil` to let the testing library decide + /// which image format to use. + /// - encodingQuality: The encoding quality to use when encoding the image. + /// If the image format used for encoding (specified by the `contentType` + /// argument) does not support variable-quality encoding, the value of + /// this argument is ignored. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// This is the designated initializer for this type when attaching an image + /// that conforms to ``AttachableAsCGImage``. + fileprivate init( + attachableValue: T, + named preferredName: String?, + as contentType: (any Sendable)?, + encodingQuality: Float, + sourceLocation: SourceLocation + ) where AttachableValue == _AttachableImageContainer { + var imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality) + + // Update the preferred name to include an extension appropriate for the + // given content type. (Note the `else` branch duplicates the logic in + // `preferredContentType(forEncodingQuality:)` but will go away once our + // minimum deployment targets include the UniformTypeIdentifiers framework.) + var preferredName = preferredName ?? Self.defaultPreferredName + if #available(_uttypesAPI, *) { + let contentType: UTType = contentType + .map { $0 as! UTType } + .flatMap { contentType in + if UTType.image.conforms(to: contentType) { + // This type is an abstract base type of .image (or .image itself.) + // We'll infer the concrete type based on other arguments. + return nil + } + return contentType + } ?? .preferred(forEncodingQuality: encodingQuality) + preferredName = (preferredName as NSString).appendingPathExtension(for: contentType) + imageContainer.contentType = contentType + } else { + // The caller can't provide a content type, so we'll pick one for them. + let ext = if encodingQuality < 1.0 { + "jpg" + } else { + "png" + } + if (preferredName as NSString).pathExtension.caseInsensitiveCompare(ext) != .orderedSame { + preferredName = (preferredName as NSString).appendingPathExtension(ext) ?? preferredName + } + } + + self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation) + } + + /// Initialize an instance of this type that encloses the given image. + /// + /// - Parameters: + /// - attachableValue: The value that will be attached to the output of + /// the test run. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the testing library attempts + /// to derive a reasonable filename for the attached value. + /// - contentType: The image format with which to encode `attachableValue`. + /// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), + /// the result is undefined. Pass `nil` to let the testing library decide + /// which image format to use. + /// - encodingQuality: The encoding quality to use when encoding the image. + /// If the image format used for encoding (specified by the `contentType` + /// argument) does not support variable-quality encoding, the value of + /// this argument is ignored. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// The following system-provided image types conform to the + /// ``AttachableAsCGImage`` protocol and can be attached to a test: + /// + /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + @_spi(Experimental) + @available(_uttypesAPI, *) + public init( + _ attachableValue: T, + named preferredName: String? = nil, + as contentType: UTType?, + encodingQuality: Float = 1.0, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageContainer { + self.init(attachableValue: attachableValue, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + } + + /// Initialize an instance of this type that encloses the given image. + /// + /// - Parameters: + /// - attachableValue: The value that will be attached to the output of + /// the test run. + /// - preferredName: The preferred name of the attachment when writing it + /// to a test report or to disk. If `nil`, the testing library attempts + /// to derive a reasonable filename for the attached value. + /// - encodingQuality: The encoding quality to use when encoding the image. + /// If the image format used for encoding (specified by the `contentType` + /// argument) does not support variable-quality encoding, the value of + /// this argument is ignored. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// The following system-provided image types conform to the + /// ``AttachableAsCGImage`` protocol and can be attached to a test: + /// + /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) + @_spi(Experimental) + public init( + _ attachableValue: T, + named preferredName: String? = nil, + encodingQuality: Float = 1.0, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageContainer { + self.init(attachableValue: attachableValue, named: preferredName, as: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift new file mode 100644 index 000000000..944798d39 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +public import CoreGraphics + +@_spi(Experimental) +extension CGImage: AttachableAsCGImage { + public var attachableCGImage: CGImage { + self + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift new file mode 100644 index 000000000..d1c7fbe7f --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift @@ -0,0 +1,39 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +/// A type representing errors that can occur when attaching an image. +package enum ImageAttachmentError: Error, CustomStringConvertible { + /// The specified content type did not conform to `.image`. + case contentTypeDoesNotConformToImage + + /// The image could not be converted to an instance of `CGImage`. + case couldNotCreateCGImage + + /// The image destination could not be created. + case couldNotCreateImageDestination + + /// The image could not be converted. + case couldNotConvertImage + + package var description: String { + switch self { + case .contentTypeDoesNotConformToImage: + "The specified type does not represent an image format." + case .couldNotCreateCGImage: + "Could not create the corresponding Core Graphics image." + case .couldNotCreateImageDestination: + "Could not create the Core Graphics image destination to encode this image." + case .couldNotConvertImage: + "Could not convert the image to the specified format." + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift new file mode 100644 index 000000000..84c7a46d1 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift @@ -0,0 +1,163 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) +@_spi(Experimental) public import Testing +private import CoreGraphics + +private import ImageIO +import UniformTypeIdentifiers + +/// ## Why can't images directly conform to Attachable? +/// +/// Three reasons: +/// +/// 1. Several image classes are not marked `Sendable`, which means that as far +/// as Swift is concerned, they cannot be safely passed to Swift Testing's +/// event handler (primarily because `Event` is `Sendable`.) So we would have +/// to eagerly serialize them, which is unnecessarily expensive if we know +/// they're actually concurrency-safe. +/// 2. We would have no place to store metadata such as the encoding quality +/// (although in the future we may introduce a "metadata" associated type to +/// `Attachable` that could store that info.) +/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return +/// position. As far as Swift is concerned, a non-final class cannot satisfy +/// such a requirement, and all image types we care about are non-final +/// classes. Thus, the compiler will steadfastly refuse to allow non-final +/// classes to conform to the `Attachable` protocol. We could get around this +/// by changing the signature of `withUnsafeBufferPointer()` so that the +/// generic parameter to `Attachment` is not `Self`, but that would defeat +/// much of the purpose of making `Attachment` generic in the first place. +/// (And no, the language does not let us write `where T: Self` anywhere +/// useful.) + +/// A wrapper type for image types such as `CGImage` and `NSImage` that can be +/// attached indirectly. +/// +/// You do not need to use this type directly. Instead, initialize an instance +/// of ``Attachment`` using an instance of an image type that conforms to +/// ``AttachableAsCGImage``. The following system-provided image types conform +/// to the ``AttachableAsCGImage`` protocol and can be attached to a test: +/// +/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) +@_spi(Experimental) +public struct _AttachableImageContainer: Sendable where ImageClass: AttachableAsCGImage { + /// The underlying image. + /// + /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` + /// instances can be created from closures that are run at rendering time. + /// The AppKit cross-import overlay is responsible for ensuring that any + /// instances of this type it creates hold "safe" `NSImage` instances. + nonisolated(unsafe) var image: ImageClass + + /// The encoding quality to use when encoding the represented image. + public var encodingQuality: Float + + /// Storage for ``contentType``. + private var _contentType: (any Sendable)? + + /// The content type to use when encoding the image. + /// + /// This property should eventually move up to ``Attachment``. It is not part + /// of the public interface of the testing library. + @available(_uttypesAPI, *) + var contentType: UTType? { + get { + _contentType as? UTType + } + set { + _contentType = newValue + } + } + + init(image: ImageClass, encodingQuality: Float) { + self.image = image._makeCopyForAttachment() + self.encodingQuality = encodingQuality + } +} + +// MARK: - + +@available(_uttypesAPI, *) +extension UTType { + /// Determine the preferred content type to encode this image as for a given + /// encoding quality. + /// + /// - Parameters: + /// - encodingQuality: The encoding quality to use when encoding the image. + /// + /// - Returns: The type to encode this image as. + static func preferred(forEncodingQuality encodingQuality: Float) -> Self { + // If the caller wants lossy encoding, use JPEG. + if encodingQuality < 1.0 { + return .jpeg + } + + // Lossless encoding implies PNG. + return .png + } +} + +extension _AttachableImageContainer: AttachableContainer { + public var attachableValue: ImageClass { + image + } + + public func withUnsafeBufferPointer(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + let data = NSMutableData() + + // Convert the image to a CGImage. + let attachableCGImage = try image.attachableCGImage + + // Get the type to encode as. (Note the `else` branches duplicate the logic + // in `preferredContentType(forEncodingQuality:)` but will go away once our + // minimum deployment targets include the UniformTypeIdentifiers framework.) + let typeIdentifier: CFString + if #available(_uttypesAPI, *), let contentType { + guard contentType.conforms(to: .image) else { + throw ImageAttachmentError.contentTypeDoesNotConformToImage + } + typeIdentifier = contentType.identifier as CFString + } else if encodingQuality < 1.0 { + typeIdentifier = kUTTypeJPEG + } else { + typeIdentifier = kUTTypePNG + } + + // Create the image destination. + guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else { + throw ImageAttachmentError.couldNotCreateImageDestination + } + + // Configure the properties of the image conversion operation. + let orientation = image._attachmentOrientation + let scaleFactor = image._attachmentScaleFactor + let properties: [CFString: Any] = [ + kCGImageDestinationLossyCompressionQuality: CGFloat(encodingQuality), + kCGImagePropertyOrientation: orientation, + kCGImagePropertyDPIWidth: 72.0 * scaleFactor, + kCGImagePropertyDPIHeight: 72.0 * scaleFactor, + ] + + // Perform the image conversion. + CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary) + guard CGImageDestinationFinalize(dest) else { + throw ImageAttachmentError.couldNotConvertImage + } + + // Pass the bits of the image out to the body. Note that we have an + // NSMutableData here so we have to use slightly different API than we would + // with an instance of Data. + return try withExtendedLifetime(data) { + try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length)) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift b/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift new file mode 100644 index 000000000..3faa622d7 --- /dev/null +++ b/Sources/Overlays/_Testing_CoreGraphics/ReexportTesting.swift @@ -0,0 +1,11 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +@_exported public import Testing diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 431e08d24..417d3dcae 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -14,6 +14,13 @@ private import _TestingInternals import Foundation @_spi(Experimental) import _Testing_Foundation #endif +#if canImport(CoreGraphics) +import CoreGraphics +@_spi(Experimental) import _Testing_CoreGraphics +#endif +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers +#endif @Suite("Attachment Tests") struct AttachmentTests { @@ -446,6 +453,87 @@ extension AttachmentTests { } } +extension AttachmentTests { + @Suite("Image tests") + struct ImageTests { + enum ImageTestError: Error { + case couldNotCreateCGContext + case couldNotCreateCGGradient + case couldNotCreateCGImage + } + +#if canImport(CoreGraphics) + static let cgImage = Result { + let size = CGSize(width: 32.0, height: 32.0) + let rgb = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGImageAlphaInfo.premultipliedFirst.rawValue + guard let context = CGContext( + data: nil, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: Int(size.width) * 4, + space: rgb, + bitmapInfo: bitmapInfo + ) else { + throw ImageTestError.couldNotCreateCGContext + } + guard let gradient = CGGradient( + colorsSpace: rgb, + colors: [ + CGColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0), + CGColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0), + CGColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0), + ] as CFArray, + locations: nil + ) else { + throw ImageTestError.couldNotCreateCGGradient + } + context.drawLinearGradient( + gradient, + start: .zero, + end: CGPoint(x: size.width, y: size.height), + options: [.drawsBeforeStartLocation, .drawsAfterEndLocation] + ) + guard let image = context.makeImage() else { + throw ImageTestError.couldNotCreateCGImage + } + return image + } + + @available(_uttypesAPI, *) + @Test func attachCGImage() throws { + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond") + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in + #expect(buffer.count > 32) + } + attachment.attach() + } + + @available(_uttypesAPI, *) + @Test(arguments: [Float(0.0).nextUp, 0.25, 0.5, 0.75, 1.0], [.png as UTType?, .jpeg, .gif, .image, .data, nil]) + func attachCGImage(quality: Float, type: UTType?) throws { + let image = try Self.cgImage.get() + let attachment = Attachment(image, named: "diamond", as: type, encodingQuality: quality) + #expect(attachment.attachableValue === image) + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { buffer in + #expect(buffer.count > 32) + } + } + + @available(_uttypesAPI, *) + @Test func cannotAttachCGImageWithNonImageType() async { + #expect(throws: ImageAttachmentError.contentTypeDoesNotConformToImage) { + let attachment = Attachment(try Self.cgImage.get(), named: "diamond", as: .mp3) + try attachment.attachableValue.withUnsafeBufferPointer(for: attachment) { _ in } + } + } +#endif + } +} + // MARK: - Fixtures struct MyAttachable: Attachable, ~Copyable { From d11f84ec3fa23be1fdc45fde9e53fb93e9d0e274 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 5 Dec 2024 16:41:15 -0800 Subject: [PATCH 2/3] Incorporate feedback --- .../Attachments/Attachment+AttachableAsCGImage.swift | 6 +++--- .../Attachments/ImageAttachmentError.swift | 2 +- .../Attachments/_AttachableImageContainer.swift | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index bb963646b..f93afb7f7 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -39,7 +39,7 @@ extension Attachment { fileprivate init( attachableValue: T, named preferredName: String?, - as contentType: (any Sendable)?, + contentType: (any Sendable)?, encodingQuality: Float, sourceLocation: SourceLocation ) where AttachableValue == _AttachableImageContainer { @@ -111,7 +111,7 @@ extension Attachment { encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageContainer { - self.init(attachableValue: attachableValue, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) } /// Initialize an instance of this type that encloses the given image. @@ -141,7 +141,7 @@ extension Attachment { encodingQuality: Float = 1.0, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageContainer { - self.init(attachableValue: attachableValue, named: preferredName, as: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) + self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) } } #endif diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift index d1c7fbe7f..1d9a4e825 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift @@ -9,7 +9,7 @@ // #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) -/// A type representing errors that can occur when attaching an image. +/// A type representing an error that can occur when attaching an image. package enum ImageAttachmentError: Error, CustomStringConvertible { /// The specified content type did not conform to `.image`. case contentTypeDoesNotConformToImage diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift index 84c7a46d1..5e8fcd227 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageContainer.swift @@ -25,7 +25,7 @@ import UniformTypeIdentifiers /// to eagerly serialize them, which is unnecessarily expensive if we know /// they're actually concurrency-safe. /// 2. We would have no place to store metadata such as the encoding quality -/// (although in the future we may introduce a "metadata" associated type to +/// (although in the future we may introduce a "metadata" associated type to /// `Attachable` that could store that info.) /// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return /// position. As far as Swift is concerned, a non-final class cannot satisfy @@ -48,14 +48,14 @@ import UniformTypeIdentifiers /// /// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) @_spi(Experimental) -public struct _AttachableImageContainer: Sendable where ImageClass: AttachableAsCGImage { +public struct _AttachableImageContainer: Sendable where Image: AttachableAsCGImage { /// The underlying image. /// /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` /// instances can be created from closures that are run at rendering time. /// The AppKit cross-import overlay is responsible for ensuring that any /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) var image: ImageClass + nonisolated(unsafe) var image: Image /// The encoding quality to use when encoding the represented image. public var encodingQuality: Float @@ -77,7 +77,7 @@ public struct _AttachableImageContainer: Sendable where ImageClass: } } - init(image: ImageClass, encodingQuality: Float) { + init(image: Image, encodingQuality: Float) { self.image = image._makeCopyForAttachment() self.encodingQuality = encodingQuality } @@ -106,7 +106,7 @@ extension UTType { } extension _AttachableImageContainer: AttachableContainer { - public var attachableValue: ImageClass { + public var attachableValue: Image { image } From 2ddf2b683e4bae7d9c8e3f6f68cbc3c69aea50dd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 6 Dec 2024 10:20:29 -0800 Subject: [PATCH 3/3] Use SPI instead of package (for now) --- .../Attachments/ImageAttachmentError.swift | 6 ++++-- Tests/TestingTests/AttachmentTests.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift index 1d9a4e825..fdd0b3f3e 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift @@ -10,7 +10,8 @@ #if SWT_TARGET_OS_APPLE && canImport(CoreGraphics) /// A type representing an error that can occur when attaching an image. -package enum ImageAttachmentError: Error, CustomStringConvertible { +@_spi(ForSwiftTestingOnly) +public enum ImageAttachmentError: Error, CustomStringConvertible { /// The specified content type did not conform to `.image`. case contentTypeDoesNotConformToImage @@ -23,7 +24,8 @@ package enum ImageAttachmentError: Error, CustomStringConvertible { /// The image could not be converted. case couldNotConvertImage - package var description: String { + @_spi(ForSwiftTestingOnly) + public var description: String { switch self { case .contentTypeDoesNotConformToImage: "The specified type does not represent an image format." diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 417d3dcae..a4a5b5104 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -16,7 +16,7 @@ import Foundation #endif #if canImport(CoreGraphics) import CoreGraphics -@_spi(Experimental) import _Testing_CoreGraphics +@_spi(Experimental) @_spi(ForSwiftTestingOnly) import _Testing_CoreGraphics #endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers