|
| 1 | +# Image attachments in Swift Testing (Windows) |
| 2 | + |
| 3 | +* Proposal: [ST-NNNN](NNNN-filename.md) |
| 4 | +* Authors: [Jonathan Grynspan](https://github.com/grynspan) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting review** |
| 7 | +* Implementation: [swiftlang/swift-testing#1245](https://github.com/swiftlang/swift-testing/pull/1245), [swiftlang/swift-testing#1254](https://github.com/swiftlang/swift-testing/pull/1254), _et al_. |
| 8 | +* Review: ([pitch](https://forums.swift.org/...)) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +In [ST-0014](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0014-image-attachments-in-swift-testing-apple-platforms.md), |
| 13 | +we added to Swift Testing the ability to attach images (of types `CGImage`, |
| 14 | +`NSImage`, `UIImage`, and `CIImage`) on Apple platforms. This proposal builds on |
| 15 | +that one to add support for attaching images on Windows. |
| 16 | + |
| 17 | +## Motivation |
| 18 | + |
| 19 | +It is frequently useful to be able to attach images to tests for engineers to |
| 20 | +review, e.g. if a UI element is not being drawn correctly. If something doesn't |
| 21 | +render correctly in a CI environment, for instance, it is very useful to test |
| 22 | +authors to be able to download the failed rendering and examine it at-desk. |
| 23 | + |
| 24 | +We recently introduced the ability to attach images to tests on Apple's |
| 25 | +platforms. Swift Testing is a cross-platform testing library, so we should |
| 26 | +extend this functionality to other platforms too. This proposal covers Windows |
| 27 | +in particular. |
| 28 | + |
| 29 | +## Proposed solution |
| 30 | + |
| 31 | +We propose adding the ability to automatically encode images to standard |
| 32 | +graphics formats such as JPEG or PNG using Windows' built-in Windows Image |
| 33 | +Component library, similar to how we added support on Apple platforms using Core |
| 34 | +Graphics. |
| 35 | + |
| 36 | +## Detailed design |
| 37 | + |
| 38 | +### Some background about Windows' image types |
| 39 | + |
| 40 | +Windows has several generations of API for representing and encoding images. The |
| 41 | +earliest Windows API of interest to this proposal is the Graphics Device |
| 42 | +Interface (GDI) which dates back to the earliest versions of Windows. Image |
| 43 | +types in GDI that are of interest to us are `HBITMAP` and `HICON`, which are |
| 44 | +_handles_ (pointers-to-pointers) and which are not reference-counted. Both types |
| 45 | +are projected into Swift as typealiases of `UnsafeMutablePointer`. |
| 46 | + |
| 47 | +Windows' latest[^direct2d] graphics API is the Windows Imaging Component (WIC) |
| 48 | +which uses types based on the Component Object Model (COM). COM types (including |
| 49 | +those implemented in WIC) are C++ classes that inherit from `IUnknown`. |
| 50 | + |
| 51 | +[^direct2d]: There is an even newer API in this area, Direct2D, but it is beyond |
| 52 | + the scope of this proposal. A developer who has an instance of e.g. |
| 53 | + `ID2D1Bitmap` can use WIC API to convert it to a WIC bitmap source before |
| 54 | + attaching it to a test. |
| 55 | + |
| 56 | +`IUnknown` which is conceptually similar to Cocoa's `NSObject` class in that it |
| 57 | +provides basic reference-counting and reflection functionality. As of this |
| 58 | +proposal, the Swift C/C++ importer is not aware of COM classes and does not |
| 59 | +project them into Swift as reference-counted classes. Rather, they are projected |
| 60 | +as `UnsafeMutablePointer<T>`, and developers who use them must manually manage |
| 61 | +their reference counts and must use `QueryInterface()` to cast them to other COM |
| 62 | +classes. |
| 63 | + |
| 64 | +In short: the types we need to support are all specializations of |
| 65 | +`UnsafeMutablePointer`, but we do not need to support all specializations of |
| 66 | +`UnsafeMutablePointer` unconditionally. |
| 67 | + |
| 68 | +### Defining a new protocol for Windows image attachments |
| 69 | + |
| 70 | +A new protocol is introduced for Windows, similar to the `AttachableAsCGImage` |
| 71 | +protocol we introduced for Apple's platforms: |
| 72 | + |
| 73 | +```swift |
| 74 | +/// A protocol describing images that can be converted to instances of |
| 75 | +/// ``Testing/Attachment``. |
| 76 | +/// |
| 77 | +/// Instances of types conforming to this protocol do not themselves conform to |
| 78 | +/// ``Testing/Attachable``. Instead, the testing library provides additional |
| 79 | +/// initializers on ``Testing/Attachment`` that take instances of such types and |
| 80 | +/// handle converting them to image data when needed. |
| 81 | +/// |
| 82 | +/// The following system-provided image types conform to this protocol and can |
| 83 | +/// be attached to a test: |
| 84 | +/// |
| 85 | +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) |
| 86 | +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) |
| 87 | +/// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) |
| 88 | +/// (including its subclasses declared by Windows Imaging Component) |
| 89 | +/// |
| 90 | +/// You do not generally need to add your own conformances to this protocol. If |
| 91 | +/// you have an image in another format that needs to be attached to a test, |
| 92 | +/// first convert it to an instance of one of the types above. |
| 93 | +public protocol AttachableAsIWICBitmapSource { |
| 94 | + /// Create a WIC bitmap source representing an instance of this type. |
| 95 | + /// |
| 96 | + /// - Returns: A pointer to a new WIC bitmap source representing this image. |
| 97 | + /// The caller is responsible for releasing this image when done with it. |
| 98 | + /// |
| 99 | + /// - Throws: Any error that prevented the creation of the WIC bitmap source. |
| 100 | + func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer<IWICBitmapSource> |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +And we add a conformance to this protocol to `UnsafeMutablePointer` when its |
| 105 | +`Pointee` type is one of the following types: |
| 106 | + |
| 107 | +- [`HBITMAP.Pointee`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) |
| 108 | +- [`HICON.Pointee`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) |
| 109 | +- [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) |
| 110 | + (including its subclasses declared by Windows Imaging Component) |
| 111 | + |
| 112 | +> [!NOTE] |
| 113 | +> The list of conforming types may be extended in the future. The Testing |
| 114 | +> Workgroup will determine if additional Swift Evolution reviews are needed. |
| 115 | +
|
| 116 | +A type in Swift can only conform to a protocol with **one** set of constraints, |
| 117 | +so we need a helper protocol in order to make `UnsafeMutablePointer` |
| 118 | +conditionally conform for all of the above types. This protocol must be `public` |
| 119 | +so that Swift Testing can refer to it in API, but it is an implementation detail |
| 120 | +and not part of this proposal: |
| 121 | + |
| 122 | +```swift |
| 123 | +public protocol _AttachableByAddressAsIWICBitmapSource {} |
| 124 | + |
| 125 | +extension HBITMAP.Pointee: _AttachableByAddressAsIWICBitmapSource {} |
| 126 | +extension HICON.Pointee: _AttachableByAddressAsIWICBitmapSource {} |
| 127 | +extension IWICBitmapSource: _AttachableByAddressAsIWICBitmapSource {} |
| 128 | + |
| 129 | +extension UnsafeMutablePointer: AttachableAsIWICBitmapSource |
| 130 | + where Pointee: _AttachableByAddressAsIWICBitmapSource {} |
| 131 | +``` |
| 132 | + |
| 133 | +See the **Future directions** section (specifically the point about COM and C++ |
| 134 | +interop) for more information on why the helper protocol is excluded from this |
| 135 | +proposal. |
| 136 | + |
| 137 | +### Attaching a conforming image |
| 138 | + |
| 139 | +New overloads of `Attachment.init()` and `Attachment.record()` are provided: |
| 140 | + |
| 141 | +```swift |
| 142 | +extension Attachment { |
| 143 | + /// Initialize an instance of this type that encloses the given image. |
| 144 | + /// |
| 145 | + /// - Parameters: |
| 146 | + /// - image: A pointer to the value that will be attached to the output of |
| 147 | + /// the test run. |
| 148 | + /// - preferredName: The preferred name of the attachment when writing it |
| 149 | + /// to a test report or to disk. If `nil`, the testing library attempts |
| 150 | + /// to derive a reasonable filename for the attached value. |
| 151 | + /// - imageFormat: The image format with which to encode `image`. |
| 152 | + /// - sourceLocation: The source location of the call to this initializer. |
| 153 | + /// This value is used when recording issues associated with the |
| 154 | + /// attachment. |
| 155 | + /// |
| 156 | + /// The following system-provided image types conform to the |
| 157 | + /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: |
| 158 | + /// |
| 159 | + /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) |
| 160 | + /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) |
| 161 | + /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) |
| 162 | + /// (including its subclasses declared by Windows Imaging Component) |
| 163 | + /// |
| 164 | + /// The testing library uses the image format specified by `imageFormat`. Pass |
| 165 | + /// `nil` to let the testing library decide which image format to use. If you |
| 166 | + /// pass `nil`, then the image format that the testing library uses depends on |
| 167 | + /// the path extension you specify in `preferredName`, if any. If you do not |
| 168 | + /// specify a path extension, or if the path extension you specify doesn't |
| 169 | + /// correspond to an image format the operating system knows how to write, the |
| 170 | + /// testing library selects an appropriate image format for you. |
| 171 | + public init<T>( |
| 172 | + _ image: T, |
| 173 | + named preferredName: String? = nil, |
| 174 | + as imageFormat: AttachableImageFormat? = nil, |
| 175 | + sourceLocation: SourceLocation = #_sourceLocation |
| 176 | + ) where AttachableValue == _AttachableImageWrapper<T> |
| 177 | + |
| 178 | + /// Attach an image to the current test. |
| 179 | + /// |
| 180 | + /// - Parameters: |
| 181 | + /// - image: The value to attach. |
| 182 | + /// - preferredName: The preferred name of the attachment when writing it |
| 183 | + /// to a test report or to disk. If `nil`, the testing library attempts |
| 184 | + /// to derive a reasonable filename for the attached value. |
| 185 | + /// - imageFormat: The image format with which to encode `image`. |
| 186 | + /// - sourceLocation: The source location of the call to this initializer. |
| 187 | + /// This value is used when recording issues associated with the |
| 188 | + /// attachment. |
| 189 | + /// |
| 190 | + /// This function creates a new instance of ``Attachment`` wrapping `image` |
| 191 | + /// and immediately attaches it to the current test. |
| 192 | + /// |
| 193 | + /// The following system-provided image types conform to the |
| 194 | + /// ``AttachableAsIWICBitmapSource`` protocol and can be attached to a test: |
| 195 | + /// |
| 196 | + /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) |
| 197 | + /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) |
| 198 | + /// - [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) |
| 199 | + /// (including its subclasses declared by Windows Imaging Component) |
| 200 | + /// |
| 201 | + /// The testing library uses the image format specified by `imageFormat`. Pass |
| 202 | + /// `nil` to let the testing library decide which image format to use. If you |
| 203 | + /// pass `nil`, then the image format that the testing library uses depends on |
| 204 | + /// the path extension you specify in `preferredName`, if any. If you do not |
| 205 | + /// specify a path extension, or if the path extension you specify doesn't |
| 206 | + /// correspond to an image format the operating system knows how to write, the |
| 207 | + /// testing library selects an appropriate image format for you. |
| 208 | + public static func record<T>( |
| 209 | + _ image: T, |
| 210 | + named preferredName: String? = nil, |
| 211 | + as imageFormat: AttachableImageFormat? = nil, |
| 212 | + sourceLocation: SourceLocation = #_sourceLocation |
| 213 | + ) where AttachableValue == _AttachableImageWrapper<T> |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +> [!NOTE] |
| 218 | +> `_AttachableImageWrapper` was described in [ST-0014](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0014-image-attachments-in-swift-testing-apple-platforms.md#attaching-a-conforming-image). |
| 219 | +> The only difference on Windows is that its associated `Image` type is |
| 220 | +> constrained to `AttachableAsIWICBitmapSource` instead of `AttachableAsCGImage`. |
| 221 | +
|
| 222 | +### Specifying image formats |
| 223 | + |
| 224 | +As on Apple platforms, a test author can specify the image format to use with |
| 225 | +`AttachableImageFormat`. See [ST-0014](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0014-image-attachments-in-swift-testing-apple-platforms.md#specifying-image-formats) |
| 226 | +for more information about that type. |
| 227 | + |
| 228 | +Windows does not use Uniform Type Identifiers, so those `AttachableImageFormat` |
| 229 | +members that use `UTType` are not available here. Instead, Windows uses a |
| 230 | +variety of COM classes that implement codecs for different image formats. We |
| 231 | +provide conveniences over those COM classes' `CLSID` values: |
| 232 | + |
| 233 | +```swift |
| 234 | +extension AttachableImageFormat { |
| 235 | + /// The `CLSID` value corresponding to the WIC image encoder for this image |
| 236 | + /// format. |
| 237 | + public var clsid: CLSID { get } |
| 238 | + |
| 239 | + /// Construct an instance of this type with the given `CLSID` value and |
| 240 | + /// encoding quality. |
| 241 | + /// |
| 242 | + /// - Parameters: |
| 243 | + /// - clsid: The `CLSID` value corresponding to a WIC image encoder to use |
| 244 | + /// when encoding images. |
| 245 | + /// - encodingQuality: The encoding quality to use when encoding images. For |
| 246 | + /// the lowest supported quality, pass `0.0`. For the highest supported |
| 247 | + /// quality, pass `1.0`. |
| 248 | + /// |
| 249 | + /// If the target image encoder does not support variable-quality encoding, |
| 250 | + /// the value of the `encodingQuality` argument is ignored. |
| 251 | + /// |
| 252 | + /// If `clsid` does not represent an image encoder type supported by WIC, the |
| 253 | + /// result is undefined. For a list of image encoders supported by WIC, see |
| 254 | + /// the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) |
| 255 | + /// class. |
| 256 | + public init(_ clsid: CLSID, encodingQuality: Float = 1.0) |
| 257 | +} |
| 258 | +``` |
| 259 | + |
| 260 | +For convenience, we also provide an initializer that takes a path extension |
| 261 | +and tries to map it to the appropriate codec's `CLSID` value: |
| 262 | + |
| 263 | +```swift |
| 264 | +extension AttachableImageFormat { |
| 265 | + /// Construct an instance of this type with the given path extension and |
| 266 | + /// encoding quality. |
| 267 | + /// |
| 268 | + /// - Parameters: |
| 269 | + /// - pathExtension: A path extension corresponding to the image format to |
| 270 | + /// use when encoding images. |
| 271 | + /// - encodingQuality: The encoding quality to use when encoding images. For |
| 272 | + /// the lowest supported quality, pass `0.0`. For the highest supported |
| 273 | + /// quality, pass `1.0`. |
| 274 | + /// |
| 275 | + /// If the target image format does not support variable-quality encoding, |
| 276 | + /// the value of the `encodingQuality` argument is ignored. |
| 277 | + /// |
| 278 | + /// If `pathExtension` does not correspond to an image format that WIC can use |
| 279 | + /// to encode images, this initializer returns `nil`. For a list of image |
| 280 | + /// encoders supported by WIC, see the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) |
| 281 | + /// class. |
| 282 | + public init?(pathExtension: String, encodingQuality: Float = 1.0) |
| 283 | +} |
| 284 | +``` |
| 285 | + |
| 286 | +For consistency, we will expose `init(pathExtension:encodingQuality:)` on Apple |
| 287 | +platforms too. (This is the only part of this proposal that affects platforms |
| 288 | +other than Windows.) |
| 289 | + |
| 290 | +### Example usage |
| 291 | + |
| 292 | +A developer may then easily attach an image to a test by calling |
| 293 | +`Attachment.record()` and passing the image of interest. For example, to attach |
| 294 | +an icon to a test as a PNG file: |
| 295 | + |
| 296 | +```swift |
| 297 | +import Testing |
| 298 | +import WinSDK |
| 299 | + |
| 300 | +@MainActor @Test func `attaching an icon`() throws { |
| 301 | + let hIcon: HICON = ... |
| 302 | + defer { |
| 303 | + DestroyIcon(hIcon) |
| 304 | + } |
| 305 | + Attachment.record(hIcon, named: "my icon", as: .png) |
| 306 | + // OR: Attachment.record(hIcon, named: "my icon.png") |
| 307 | +} |
| 308 | +``` |
| 309 | + |
| 310 | +## Source compatibility |
| 311 | + |
| 312 | +This change is additive only. |
| 313 | + |
| 314 | +## Integration with supporting tools |
| 315 | + |
| 316 | +None needed. |
| 317 | + |
| 318 | +## Future directions |
| 319 | + |
| 320 | +- Adding support for projecting COM classes as foreign-reference-counted Swift |
| 321 | + classes. The C++ interop team is interested in implementing this feature, but |
| 322 | + it is beyond the scope of this proposal. **If this feature is implemented in |
| 323 | + the future**, it will cause types like `IWICBitmapSource` to be projected |
| 324 | + directly into Swift instead of as `UnsafeMutablePointer` specializations. This |
| 325 | + would be a source-breaking change for Swift Testing, but it would make COM |
| 326 | + classes much easier to use in Swift. |
| 327 | + |
| 328 | + In the context of this proposal, `IWICBitmapSource` would be able to directly |
| 329 | + conform to `AttachableAsIWICBitmapSource` and we would no longer need the |
| 330 | + `_AttachableByAddressAsIWICBitmapSource` helper protocol. The |
| 331 | + `AttachableAsIWICBitmapSource` protocol's `copyAttachableIWICBitmapSource()` |
| 332 | + requirement would likely change to a property (i.e. |
| 333 | + `var attachableIWICBitmapSource: IWICBitmapSource { get throws }`) as it would |
| 334 | + be able to participate in Swift's automatic reference counting. |
| 335 | + |
| 336 | +- Adding support for managed (.NET or C#) image types. Support for managed types |
| 337 | + on Windows would first require a new Swift/.NET or Swift/C# interop feature |
| 338 | + and is therefore beyond the scope of this proposal. |
| 339 | + |
| 340 | +- Adding support for other platforms. See [ST-0014](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0014-image-attachments-in-swift-testing-apple-platforms.md#future-directions) |
| 341 | + for further discussion about supporting additional platforms. |
| 342 | + |
| 343 | +## Alternatives considered |
| 344 | + |
| 345 | +- Doing nothing. We have already added support for attaching images on Apple's |
| 346 | + platforms, and Swift Testing is meant to be a cross-platform library, so we |
| 347 | + should make a best effort to provide the same functionality on Windows and, |
| 348 | + eventually, other platforms. |
| 349 | + |
| 350 | +- Using more Windows-/COM-like terminology and spelling, e.g. |
| 351 | + `CloneAttachableBitmapSource()` instead of `copyAttachableIWICBitmapSource()`. |
| 352 | + Swift API should follow Swift API guidelines, even when extending types and |
| 353 | + calling functions implemented under other standards. |
| 354 | + |
| 355 | +- Making `IWICBitmapSource` conform directly to `Attachable`. As with `CGImage` |
| 356 | + in [ST-0014](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0014-image-attachments-in-swift-testing-apple-platforms.md#alternatives-considered), |
| 357 | + this would prevent us from including additional information (i.e. an instance |
| 358 | + of `AttachableImageFormat`). Further, it would be difficult to correctly |
| 359 | + manage the lifetime of Windows' 'image objects as they do not participate in |
| 360 | + automatic reference counting. |
| 361 | + |
| 362 | +- Using the GDI+ type [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image) |
| 363 | + as our currency type instead of `IWICBitmapSource`. This type is a C++ class |
| 364 | + but is not a COM class, and so it is not projected into Swift except as |
| 365 | + `OpaquePointer` which makes it unsafe to extend it with protocol conformances. |
| 366 | + As well, GDI+ is a much older API than WIC and is not recommended by Microsoft |
| 367 | + for new development. |
| 368 | + |
| 369 | +- Designing a platform-agnostic solution. This would likely require adding a |
| 370 | + dependency on an open-source image package such as [ImageMagick](https://github.com/ImageMagick/ImageMagick). |
| 371 | + Such a library would be a significant new dependency for the testing library |
| 372 | + and the Swift toolchain at large. |
| 373 | + |
| 374 | +## Acknowledgments |
| 375 | + |
| 376 | +Thank you to @compnerd and the C++ interop team for their help with Windows and |
| 377 | +the COM API. |
0 commit comments