Skip to content

Commit fd84e29

Browse files
committed
Apply changes from ST-NNNN.
This PR applies the changes from [ST-NNNN](swiftlang/swift-evolution#2985). It merges `AttachableAsCGImage` and `AttachableAsIWICBitmapSource` into a single `AttachableAsImage` protocol and it adjusts the interfaces of `AttachableImageFormat` and `Attachment where AttachableValue: AttachableAsImage`.
1 parent 08f39d6 commit fd84e29

27 files changed

+505
-521
lines changed

Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsCGImage.swift renamed to Sources/Overlays/_Testing_AppKit/Attachments/NSImage+AttachableAsImage.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,21 @@ extension NSImageRep {
3636
/// @Metadata {
3737
/// @Available(Swift, introduced: 6.3)
3838
/// }
39-
extension NSImage: AttachableAsCGImage {
39+
extension NSImage: AttachableAsImage, AttachableAsCGImage {
4040
/// @Metadata {
4141
/// @Available(Swift, introduced: 6.3)
4242
/// }
43-
public var attachableCGImage: CGImage {
43+
package var attachableCGImage: CGImage {
4444
get throws {
45-
let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform
45+
let ctm = AffineTransform(scale: attachmentScaleFactor) as NSAffineTransform
4646
guard let result = cgImage(forProposedRect: nil, context: nil, hints: [.ctm: ctm]) else {
4747
throw ImageAttachmentError.couldNotCreateCGImage
4848
}
4949
return result
5050
}
5151
}
5252

53-
public var _attachmentScaleFactor: CGFloat {
53+
package var attachmentScaleFactor: CGFloat {
5454
let maxRepWidth = representations.lazy
5555
.map { CGFloat($0.pixelsWide) / $0.size.width }
5656
.filter { $0 > 0.0 }

Sources/Overlays/_Testing_AppKit/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
1010
add_library(_Testing_AppKit
11-
Attachments/NSImage+AttachableAsCGImage.swift
11+
Attachments/NSImage+AttachableAsImage.swift
1212
ReexportTesting.swift)
1313

1414
target_link_libraries(_Testing_AppKit PUBLIC

Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,21 @@
99
//
1010

1111
#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
12-
public import CoreGraphics
13-
private import ImageIO
12+
package import CoreGraphics
13+
package import ImageIO
14+
private import UniformTypeIdentifiers
1415

1516
/// A protocol describing images that can be converted to instances of
16-
/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment).
17+
/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment)
18+
/// and which can be represented as instances of [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage).
1719
///
18-
/// Instances of types conforming to this protocol do not themselves conform to
19-
/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable).
20-
/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment)
21-
/// that take instances of such types and handle converting them to image data when needed.
22-
///
23-
/// You can attach instances of the following system-provided image types to a
24-
/// test:
25-
///
26-
/// | Platform | Supported Types |
27-
/// |-|-|
28-
/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) |
29-
/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) |
30-
/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) |
31-
///
32-
/// You do not generally need to add your own conformances to this protocol. If
33-
/// you have an image in another format that needs to be attached to a test,
34-
/// first convert it to an instance of one of the types above.
35-
///
36-
/// @Metadata {
37-
/// @Available(Swift, introduced: 6.3)
38-
/// }
20+
/// This protocol is not part of the public interface of the testing library. It
21+
/// encapsulates Apple-specific logic for image attachments.
3922
@available(_uttypesAPI, *)
40-
public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype {
23+
package protocol AttachableAsCGImage: AttachableAsImage, SendableMetatype {
4124
/// An instance of `CGImage` representing this image.
4225
///
4326
/// - Throws: Any error that prevents the creation of an image.
44-
///
45-
/// @Metadata {
46-
/// @Available(Swift, introduced: 6.3)
47-
/// }
4827
var attachableCGImage: CGImage { get throws }
4928

5029
/// The orientation of the image.
@@ -53,38 +32,64 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype {
5332
/// `CGImagePropertyOrientation`. The default value of this property is
5433
/// `.up`.
5534
///
56-
/// This property is not part of the public interface of the testing
57-
/// library. It may be removed in a future update.
58-
var _attachmentOrientation: UInt32 { get }
35+
/// This property is not part of the public interface of the testing library.
36+
/// It may be removed in a future update.
37+
var attachmentOrientation: CGImagePropertyOrientation { get }
5938

6039
/// The scale factor of the image.
6140
///
6241
/// The value of this property is typically greater than `1.0` when an image
6342
/// originates from a Retina Display screenshot or similar. The default value
6443
/// of this property is `1.0`.
6544
///
66-
/// This property is not part of the public interface of the testing
67-
/// library. It may be removed in a future update.
68-
var _attachmentScaleFactor: CGFloat { get }
45+
/// This property is not part of the public interface of the testing library.
46+
/// It may be removed in a future update.
47+
var attachmentScaleFactor: CGFloat { get }
6948
}
7049

7150
@available(_uttypesAPI, *)
7251
extension AttachableAsCGImage {
73-
public var _attachmentOrientation: UInt32 {
74-
CGImagePropertyOrientation.up.rawValue
52+
package var attachmentOrientation: CGImagePropertyOrientation {
53+
.up
7554
}
7655

77-
public var _attachmentScaleFactor: CGFloat {
56+
package var attachmentScaleFactor: CGFloat {
7857
1.0
7958
}
8059

81-
public func _deinitializeAttachableValue() {}
82-
}
60+
public func withUnsafeBytes<R>(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
61+
let data = NSMutableData()
8362

84-
@available(_uttypesAPI, *)
85-
extension AttachableAsCGImage where Self: Sendable {
86-
public func _copyAttachableValue() -> Self {
87-
self
63+
// Convert the image to a CGImage.
64+
let attachableCGImage = try attachableCGImage
65+
66+
// Create the image destination.
67+
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else {
68+
throw ImageAttachmentError.couldNotCreateImageDestination
69+
}
70+
71+
// Configure the properties of the image conversion operation.
72+
let orientation = attachmentOrientation
73+
let scaleFactor = attachmentScaleFactor
74+
let properties: [CFString: Any] = [
75+
kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat.encodingQuality),
76+
kCGImagePropertyOrientation: orientation,
77+
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
78+
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
79+
]
80+
81+
// Perform the image conversion.
82+
CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary)
83+
guard CGImageDestinationFinalize(dest) else {
84+
throw ImageAttachmentError.couldNotConvertImage
85+
}
86+
87+
// Pass the bits of the image out to the body. Note that we have an
88+
// NSMutableData here so we have to use slightly different API than we would
89+
// with an instance of Data.
90+
return try withExtendedLifetime(data) {
91+
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
92+
}
8893
}
8994
}
9095
#endif

Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableImageFormat+UTType.swift

Lines changed: 16 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,58 +9,10 @@
99
//
1010

1111
#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
12-
public import Testing
13-
1412
public import UniformTypeIdentifiers
1513

1614
@available(_uttypesAPI, *)
1715
extension AttachableImageFormat {
18-
/// Get the content type to use when encoding the image, substituting a
19-
/// concrete type for `UTType.image` in particular.
20-
///
21-
/// - Parameters:
22-
/// - imageFormat: The image format to use, or `nil` if the developer did
23-
/// not specify one.
24-
/// - preferredName: The preferred name of the image for which a type is
25-
/// needed.
26-
///
27-
/// - Returns: An instance of `UTType` referring to a concrete image type.
28-
///
29-
/// This function is not part of the public interface of the testing library.
30-
static func computeContentType(for imageFormat: Self?, withPreferredName preferredName: String) -> UTType {
31-
guard let imageFormat else {
32-
// The developer didn't specify a type. Substitute the generic `.image`
33-
// and solve for that instead.
34-
return computeContentType(for: Self(.image, encodingQuality: 1.0), withPreferredName: preferredName)
35-
}
36-
37-
switch imageFormat.kind {
38-
case .png:
39-
return .png
40-
case .jpeg:
41-
return .jpeg
42-
case let .systemValue(contentType):
43-
let contentType = contentType as! UTType
44-
if contentType != .image {
45-
// The developer explicitly specified a type.
46-
return contentType
47-
}
48-
49-
// The developer didn't specify a concrete type, so try to derive one from
50-
// the preferred name's path extension.
51-
let pathExtension = (preferredName as NSString).pathExtension
52-
if !pathExtension.isEmpty,
53-
let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image),
54-
contentType.isDeclared {
55-
return contentType
56-
}
57-
58-
// We couldn't derive a concrete type from the path extension, so pick
59-
// between PNG and JPEG based on the encoding quality.
60-
return imageFormat.encodingQuality < 1.0 ? .jpeg : .png
61-
}
62-
}
63-
6416
/// The content type corresponding to this image format.
6517
///
6618
/// For example, if this image format equals ``png``, the value of this
@@ -100,12 +52,19 @@ extension AttachableImageFormat {
10052
/// @Metadata {
10153
/// @Available(Swift, introduced: 6.3)
10254
/// }
103-
public init(_ contentType: UTType, encodingQuality: Float = 1.0) {
104-
precondition(
105-
contentType.conforms(to: .image),
106-
"An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead."
107-
)
108-
self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality)
55+
public init(contentType: UTType, encodingQuality: Float = 1.0) {
56+
switch contentType {
57+
case .png:
58+
self.init(kind: .png, encodingQuality: encodingQuality)
59+
case .jpeg:
60+
self.init(kind: .jpeg, encodingQuality: encodingQuality)
61+
default:
62+
precondition(
63+
contentType.conforms(to: .image),
64+
"An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead."
65+
)
66+
self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality)
67+
}
10968
}
11069

11170
/// Construct an instance of this type with the given path extension and
@@ -135,11 +94,12 @@ extension AttachableImageFormat {
13594
public init?(pathExtension: String, encodingQuality: Float = 1.0) {
13695
let pathExtension = pathExtension.drop { $0 == "." }
13796

138-
guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image) else {
97+
guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image),
98+
contentType.isDeclared else {
13999
return nil
140100
}
141101

142-
self.init(contentType, encodingQuality: encodingQuality)
102+
self.init(contentType: contentType, encodingQuality: encodingQuality)
143103
}
144104
}
145105
#endif

Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift renamed to Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsImage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ public import CoreGraphics
1414
/// @Metadata {
1515
/// @Available(Swift, introduced: 6.3)
1616
/// }
17-
extension CGImage: AttachableAsCGImage {
17+
extension CGImage: AttachableAsImage, AttachableAsCGImage {
1818
/// @Metadata {
1919
/// @Available(Swift, introduced: 6.3)
2020
/// }
21-
public var attachableCGImage: CGImage {
21+
package var attachableCGImage: CGImage {
2222
self
2323
}
2424
}

Sources/Overlays/_Testing_CoreGraphics/Attachments/_AttachableImageWrapper+AttachableWrapper.swift

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,76 +9,52 @@
99
//
1010

1111
#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
12-
public import Testing
1312
private import CoreGraphics
1413

15-
private import ImageIO
1614
private import UniformTypeIdentifiers
1715

18-
/// ## Why can't images directly conform to Attachable?
19-
///
20-
/// Three reasons:
21-
///
22-
/// 1. Several image classes are not marked `Sendable`, which means that as far
23-
/// as Swift is concerned, they cannot be safely passed to Swift Testing's
24-
/// event handler (primarily because `Event` is `Sendable`.) So we would have
25-
/// to eagerly serialize them, which is unnecessarily expensive if we know
26-
/// they're actually concurrency-safe.
27-
/// 2. We would have no place to store metadata such as the encoding quality
28-
/// (although in the future we may introduce a "metadata" associated type to
29-
/// `Attachable` that could store that info.)
30-
/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return
31-
/// position. As far as Swift is concerned, a non-final class cannot satisfy
32-
/// such a requirement, and all image types we care about are non-final
33-
/// classes. Thus, the compiler will steadfastly refuse to allow non-final
34-
/// classes to conform to the `Attachable` protocol. We could get around this
35-
/// by changing the signature of `withUnsafeBytes()` so that the
36-
/// generic parameter to `Attachment` is not `Self`, but that would defeat
37-
/// much of the purpose of making `Attachment` generic in the first place.
38-
/// (And no, the language does not let us write `where T: Self` anywhere
39-
/// useful.)
40-
4116
@available(_uttypesAPI, *)
42-
extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsCGImage {
43-
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
44-
let data = NSMutableData()
45-
46-
// Convert the image to a CGImage.
47-
let attachableCGImage = try wrappedValue.attachableCGImage
48-
49-
// Create the image destination.
50-
let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName)
51-
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else {
52-
throw ImageAttachmentError.couldNotCreateImageDestination
17+
extension _AttachableImageWrapper: Attachable, AttachableWrapper where Image: AttachableAsImage {
18+
/// Get the image format to use when encoding an image, substituting a
19+
/// concrete type for `UTType.image` in particular.
20+
///
21+
/// - Parameters:
22+
/// - preferredName: The preferred name of the image for which a type is
23+
/// needed.
24+
///
25+
/// - Returns: An instance of ``AttachableImageFormat`` referring to a
26+
/// concrete image type.
27+
///
28+
/// This function is not part of the public interface of the testing library.
29+
private func _imageFormat(forPreferredName preferredName: String) -> AttachableImageFormat {
30+
if let imageFormat, case let contentType = imageFormat.contentType, contentType != .image {
31+
// The developer explicitly specified a type.
32+
return imageFormat
5333
}
5434

55-
// Configure the properties of the image conversion operation.
56-
let orientation = wrappedValue._attachmentOrientation
57-
let scaleFactor = wrappedValue._attachmentScaleFactor
58-
let properties: [CFString: Any] = [
59-
kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0),
60-
kCGImagePropertyOrientation: orientation,
61-
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
62-
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
63-
]
64-
65-
// Perform the image conversion.
66-
CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary)
67-
guard CGImageDestinationFinalize(dest) else {
68-
throw ImageAttachmentError.couldNotConvertImage
35+
// The developer didn't specify a concrete type, so try to derive one from
36+
// the preferred name's path extension.
37+
let pathExtension = (preferredName as NSString).pathExtension
38+
if !pathExtension.isEmpty,
39+
let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image),
40+
contentType.isDeclared {
41+
return AttachableImageFormat(contentType: contentType)
6942
}
7043

71-
// Pass the bits of the image out to the body. Note that we have an
72-
// NSMutableData here so we have to use slightly different API than we would
73-
// with an instance of Data.
74-
return try withExtendedLifetime(data) {
75-
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
76-
}
44+
// We couldn't derive a concrete type from the path extension, so pick
45+
// between PNG and JPEG based on the encoding quality.
46+
let encodingQuality = imageFormat?.encodingQuality ?? 1.0
47+
return encodingQuality < 1.0 ? .jpeg : .png
48+
}
49+
50+
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
51+
let imageFormat = _imageFormat(forPreferredName: attachment.preferredName)
52+
return try wrappedValue.withUnsafeBytes(as: imageFormat, body)
7753
}
7854

7955
public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String {
80-
let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName)
81-
return (suggestedName as NSString).appendingPathExtension(for: contentType)
56+
let imageFormat = _imageFormat(forPreferredName: suggestedName)
57+
return (suggestedName as NSString).appendingPathExtension(for: imageFormat.contentType)
8258
}
8359
}
8460
#endif

0 commit comments

Comments
 (0)