From c5941950077025ad71746713acfb0311b7593e5e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 6 Aug 2025 14:04:09 -0400 Subject: [PATCH 1/8] Rewrite Windows image attachments to use WIC instead of GDI+. This PR shifts our support for Windows image attachments onto WIC (Windows Imaging Component) instead of GDI+, which is an older API that supports fewer types of image object. --- Package.swift | 2 +- .../AttachableAsGDIPlusImage.swift | 129 ---------- .../Attachments/AttachableAsIWICBitmap.swift | 158 ++++++++++++ .../AttachableImageFormat+CLSID.swift | 231 ++++++++++-------- ...> Attachment+AttachableAsIWICBitmap.swift} | 10 +- .../_Testing_WinSDK/Attachments/GDI+.swift | 71 ------ .../HBITMAP+AttachableAsGDIPlusImage.swift | 27 -- .../HBITMAP+AttachableAsIWICBitmap.swift | 34 +++ .../HICON+AttachableAsIWICBitmap.swift | 34 +++ .../IWICBitmap+AttachableAsIWICBitmap.swift | 30 +++ .../Attachments/ImageAttachmentError.swift | 51 ++++ ...ablePointer+AttachableAsGDIPlusImage.swift | 24 -- ...tablePointer+AttachableAsIWICBitmap.swift} | 11 +- .../Attachments/_AttachableImageWrapper.swift | 126 ++++++---- .../Support/Additions/GUIDAdditions.swift | 37 +++ .../Additions/IPropertyBag2Additions.swift | 40 +++ .../IWICImagingFactoryAdditions.swift | 40 +++ Sources/_TestingInternals/GDI+/include/GDI+.h | 71 ------ Sources/_TestingInternals/include/Stubs.h | 18 -- .../include/module.modulemap | 7 - Tests/TestingTests/AttachmentTests.swift | 35 ++- 21 files changed, 680 insertions(+), 506 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift rename Sources/Overlays/_Testing_WinSDK/Attachments/{Attachment+AttachableAsGDIPlusImage.swift => Attachment+AttachableAsIWICBitmap.swift} (90%) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift rename Sources/Overlays/_Testing_WinSDK/Attachments/{HICON+AttachableAsGDIPlusImage.swift => UnsafeMutablePointer+AttachableAsIWICBitmap.swift} (50%) create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift delete mode 100644 Sources/_TestingInternals/GDI+/include/GDI+.h diff --git a/Package.swift b/Package.swift index 58ba9552a..80db6076c 100644 --- a/Package.swift +++ b/Package.swift @@ -254,7 +254,7 @@ let package = Package( "Testing", ], path: "Sources/Overlays/_Testing_WinSDK", - swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)] + swiftSettings: .packageSettings + .enableLibraryEvolution() ), // Utility targets: These are utilities intended for use when developing diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift deleted file mode 100644 index daaf88df1..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// 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 os(Windows) -@_spi(Experimental) import Testing -private import _TestingInternals.GDIPlus - -internal import WinSDK - -/// 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: -/// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// -/// 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 _AttachableByAddressAsGDIPlusImage { - /// Create a GDI+ image representing an instance of this type at the given - /// address. - /// - /// - Parameters: - /// - imageAddress: The address of the instance of this type. - /// - /// - Returns: A pointer to a new GDI+ image representing this image. The - /// caller is responsible for deleting this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the GDI+ image. - /// - /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That - /// type cannot be directly represented in Swift. If this function returns a - /// value of any other concrete type, the result is undefined. - /// - /// The testing library automatically calls `GdiplusStartup()` and - /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correctly configured on the current - /// thread when it is called. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer - - /// Clean up any resources at the given address. - /// - /// - Parameters: - /// - imageAddress: The address of the instance of this type. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this value. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for deleting the image returned from - /// `_copyAttachableGDIPlusImage(at:)`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) -} - -/// 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: -/// -/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) -/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) -/// -/// 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 AttachableAsGDIPlusImage { - /// Create a GDI+ image representing this instance. - /// - /// - Returns: A pointer to a new GDI+ image representing this image. The - /// caller is responsible for deleting this image when done with it. - /// - /// - Throws: Any error that prevented the creation of the GDI+ image. - /// - /// - Note: This function returns a value of C++ type `Gdiplus::Image *`. That - /// type cannot be directly represented in Swift. If this function returns a - /// value of any other concrete type, the result is undefined. - /// - /// The testing library automatically calls `GdiplusStartup()` and - /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correctly configured on the current - /// thread when it is called. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _copyAttachableGDIPlusImage() throws -> OpaquePointer - - /// Clean up any resources associated with this instance. - /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this value. The testing library - /// automatically invokes this function as needed. - /// - /// This function is not responsible for deleting the image returned from - /// `_copyAttachableGDIPlusImage()`. - /// - /// This function is not part of the public interface of the testing library. - /// It may be removed in a future update. - func _cleanUpAttachment() -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..c50e0fa36 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -0,0 +1,158 @@ +// +// 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 os(Windows) +@_spi(Experimental) import Testing + +public import WinSDK + +/// 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: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// +/// 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 _AttachableByAddressAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type at the given + /// address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer + + /// Manually deinitialize any resources at the given address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmap(from:using:)`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _deinitializeAttachment(at imageAddress: consuming UnsafeMutablePointer) +} + +/// 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: +/// +/// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) +/// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) +/// +/// 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 AttachableAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type. + /// + /// - Parameters: + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + borrowing func _copyAttachableIWICBitmap( + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer + + /// Manually deinitialize any resources associated with this image. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) associated with this image. The testing library + /// automatically invokes this function as needed. + /// + /// This function is not responsible for releasing the image returned from + /// `_copyAttachableIWICBitmap(using:)`. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + consuming func _deinitializeAttachment() +} + +extension AttachableAsIWICBitmap { + /// Create a WIC bitmap representing an instance of this type and return it as + /// an instance of `IWICBitmapSource`. + /// + /// - Parameters: + /// - factory: A WIC imaging factory that can be used to create additional + /// WIC objects. + /// + /// - Returns: A pointer to a new WIC bitmap representing this image. The + /// caller is responsible for releasing this image when done with it. + /// + /// - Throws: Any error that prevented the creation of the WIC bitmap. + /// + /// This function is a convenience over `_copyAttachableIWICBitmap(using:)` + /// that casts the result of that function to `IWICBitmapSource` (as needed + /// by WIC when it encodes the image.) + borrowing func copyAttachableIWICBitmapSource( + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + let bitmap = try _copyAttachableIWICBitmap(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } + + return try withUnsafePointer(to: IID_IWICBitmapSource) { IID_IWICBitmapSource in + var bitmapSource: UnsafeMutableRawPointer? + let rQuery = bitmap.pointee.lpVtbl.pointee.QueryInterface(bitmap, IID_IWICBitmapSource, &bitmapSource) + guard rQuery == S_OK, let bitmapSource else { + throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapSource.self, rQuery) + } + return bitmapSource.assumingMemoryBound(to: IWICBitmapSource.self) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 8985b2aeb..8323a8096 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -10,101 +10,141 @@ #if os(Windows) @_spi(Experimental) import Testing -private import _TestingInternals.GDIPlus public import WinSDK extension AttachableImageFormat { - /// The set of `ImageCodecInfo` instances known to GDI+. - /// - /// If the testing library was unable to determine the set of image formats, - /// the value of this property is `nil`. - /// - /// - Note: The type of this property is a buffer pointer rather than an array - /// because the resulting buffer owns trailing untyped memory where path - /// extensions and other fields are stored. Do not deallocate this buffer. - private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer = { - let result = try? withGDIPlus { - // Find out the size of the buffer needed. - var codecCount = UINT(0) - var byteCount = UINT(0) - let rGetSize = Gdiplus.GetImageEncodersSize(&codecCount, &byteCount) - guard rGetSize == Gdiplus.Ok else { - throw GDIPlusError.status(rGetSize) + private static let _encoderPathExtensionsByCLSID = Result<[UInt128: [String]], any Error> { + var result = [UInt128: [String]]() + + // Create an imaging factory. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create a COM enumerator over the encoders known to WIC. + var enumerator: UnsafeMutablePointer? + let rCreate = factory.pointee.lpVtbl.pointee.CreateComponentEnumerator( + factory, + DWORD(bitPattern: WICEncoder.rawValue), + DWORD(bitPattern: WICComponentEnumerateDefault.rawValue), + &enumerator + ) + guard rCreate == S_OK, let enumerator else { + throw ImageAttachmentError.wicObjectCreationFailed(IEnumUnknown.self, rCreate) + } + defer { + _ = enumerator.pointee.lpVtbl.pointee.Release(enumerator) + } + + // Loop through the iterator and extract the path extensions and CLSID of + // each encoder we find. + while true { + var nextObject: UnsafeMutablePointer? + guard S_OK == enumerator.pointee.lpVtbl.pointee.Next(enumerator, 1, &nextObject, nil), let nextObject else { + // End of loop. + break + } + defer { + _ = nextObject.pointee.lpVtbl.pointee.Release(nextObject) } - // Allocate a buffer of sufficient byte size, then bind the leading bytes - // to ImageCodecInfo. This leaves some number of trailing bytes unbound to - // any Swift type. - let result = UnsafeMutableRawBufferPointer.allocate( - byteCount: Int(byteCount), - alignment: MemoryLayout.alignment - ) - let codecBuffer = result - .prefix(MemoryLayout.stride * Int(codecCount)) - .bindMemory(to: Gdiplus.ImageCodecInfo.self) + // Cast the enumerated object to the correct/expected type. + let info = try withUnsafePointer(to: IID_IWICBitmapEncoderInfo) { IID_IWICBitmapEncoderInfo in + var info: UnsafeMutableRawPointer? + let rQuery = nextObject.pointee.lpVtbl.pointee.QueryInterface(nextObject, IID_IWICBitmapEncoderInfo, &info) + guard rQuery == S_OK, let info else { + throw ImageAttachmentError.queryInterfaceFailed(IWICBitmapEncoderInfo.self, rQuery) + } + return info.assumingMemoryBound(to: IWICBitmapEncoderInfo.self) + } + defer { + _ = info.pointee.lpVtbl.pointee.Release(info) + } - // Read the encoders list. - let rGetEncoders = Gdiplus.GetImageEncoders(codecCount, byteCount, codecBuffer.baseAddress!) - guard rGetEncoders == Gdiplus.Ok else { - result.deallocate() - throw GDIPlusError.status(rGetEncoders) + var clsid = CLSID() + guard S_OK == info.pointee.lpVtbl.pointee.GetCLSID(info, &clsid) else { + continue } - return UnsafeBufferPointer(codecBuffer) + let extensions = _pathExtensions(for: info) + result[UInt128(clsid)] = extensions } - return result ?? UnsafeBufferPointer(start: nil, count: 0) - }() + + return result + } /// Get the set of path extensions corresponding to the image format - /// represented by a GDI+ codec info structure. + /// represented by a WIC bitmap encoder info object. /// /// - Parameters: - /// - codec: The GDI+ codec info structure of interest. + /// - info: The WIC bitmap encoder info object of interest. /// /// - Returns: An array of zero or more path extensions. The case of the /// resulting strings is unspecified. - private static func _pathExtensions(for codec: Gdiplus.ImageCodecInfo) -> [String] { - guard let extensions = String.decodeCString(codec.FilenameExtension, as: UTF16.self)?.result else { + private static func _pathExtensions(for info: UnsafeMutablePointer) -> [String] { + // Figure out the size of the buffer we need. (Microsoft does not specify if + // the size is in wide characters or bytes.) + var charCount = UINT(0) + var rGet = info.pointee.lpVtbl.pointee.GetFileExtensions(info, 0, nil, &charCount) + guard rGet == S_OK else { + return [] + } + + // Allocate the necessary buffer and populate it. + let buffer = UnsafeMutableBufferPointer.allocate(capacity: Int(charCount)) + defer { + buffer.deallocate() + } + rGet = info.pointee.lpVtbl.pointee.GetFileExtensions(info, UINT(buffer.count), buffer.baseAddress!, &charCount) + guard rGet == S_OK else { + return [] + } + + // Convert the buffer to a Swift string for further manipulation. + guard let extensions = String.decodeCString(buffer.baseAddress!, as: UTF16.self)?.result else { return [] } + return extensions - .split(separator: ";") + .split(separator: ",") .map { ext in - if ext.starts(with: "*.") { - ext.dropFirst(2) + if ext.starts(with: ".") { + ext.dropFirst(1) } else { ext[...] } - }.map{ $0.lowercased() } // Vestiges of MS-DOS... + }.map(String.init) } - /// Get the `CLSID` value corresponding to the same image format as the given - /// path extension. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the given path extension. /// /// - Parameters: /// - pathExtension: The path extension (as a wide C string) for which a /// `CLSID` value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { - _allCodecs.first { codec in - _pathExtensions(for: codec) - .contains { codecExtension in - codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in - 0 == _wcsicmp(pathExtension, codecExtension) + let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] + return encoderPathExtensionsByCLSID + .first { _, extensions in + extensions.contains { encoderExt in + encoderExt.withCString(encodedAs: UTF16.self) { encoderExt in + 0 == _wcsicmp(pathExtension, encoderExt) } } - }.map(\.Clsid) + }.map { CLSID($0.key) } } - /// Get the `CLSID` value corresponding to the same image format as the given - /// path extension. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the given path extension. /// /// - Parameters: /// - pathExtension: The path extension for which a `CLSID` value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPathExtension pathExtension: String) -> CLSID? { pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in @@ -112,14 +152,14 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value corresponding to the same image format as the path - /// extension on the given attachment filename. + /// Get the `CLSID` value of the WIC image encoder corresponding to the same + /// image format as the path extension on the given attachment filename. /// /// - Parameters: /// - preferredName: The preferred name of the image for which a `CLSID` /// value is needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder, or /// `nil` if one could not be determined. private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in @@ -132,7 +172,8 @@ extension AttachableImageFormat { } } - /// Get the `CLSID` value` to use when encoding the image. + /// Get the `CLSID` value of the WIC image encoder to use when encoding an + /// image. /// /// - Parameters: /// - imageFormat: The image format to use, or `nil` if the developer did @@ -140,8 +181,9 @@ extension AttachableImageFormat { /// - preferredName: The preferred name of the image for which a type is /// needed. /// - /// - Returns: An instance of `CLSID` referring to a concrete image type, or - /// `nil` if one could not be determined. + /// - Returns: An instance of `CLSID` referring to a a WIC image encoder. If + /// none could be derived from `imageFormat` or `preferredName`, the PNG + /// encoder is used. /// /// This function is not part of the public interface of the testing library. static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { @@ -158,19 +200,19 @@ extension AttachableImageFormat { // We couldn't derive a concrete type from the path extension, so default // to PNG. Unlike Apple platforms, there's no abstract "image" type on // Windows so we don't need to make any more decisions. - return _pngCLSID + return CLSID_WICPngEncoder } - /// Append the path extension preferred by GDI+ for the given `CLSID` value - /// representing an image format to a suggested extension filename. + /// Append the path extension preferred by WIC for the image format + /// corresponding to the given `CLSID` value or the given filename. /// /// - Parameters: /// - clsid: The `CLSID` value representing the image format of interest. /// - preferredName: The preferred name of the image for which a type is /// needed. /// - /// - Returns: A string containing the corresponding path extension, or `nil` - /// if none could be determined. + /// - Returns: A copy of `preferredName`, possibly modified to include a path + /// extension appropriate for `CLSID`. static func appendPathExtension(for clsid: CLSID, to preferredName: String) -> String { // If there's already a CLSID associated with the filename, and it matches // the one passed to us, no changes are needed. @@ -178,38 +220,24 @@ extension AttachableImageFormat { return preferredName } - let ext = _allCodecs - .first { $0.Clsid == clsid } - .flatMap { _pathExtensions(for: $0).first } - guard let ext else { - // Couldn't find a path extension for the given CLSID, so make no changes. - return preferredName + // Find the preferred path extension for the encoder with the given CLSID. + let encoderPathExtensionsByCLSID = (try? _encoderPathExtensionsByCLSID.get()) ?? [:] + if let ext = encoderPathExtensionsByCLSID[UInt128(clsid)]?.first { + return "\(preferredName).\(ext)" } - return "\(preferredName).\(ext)" + // Couldn't find anything better. Return the preferred name unmodified. + return preferredName } - /// The `CLSID` value corresponding to the PNG image format. - /// - /// - Note: The named constant [`ImageFormatPNG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) - /// is not the correct value and will cause `Image::Save()` to fail if - /// passed to it. - private static let _pngCLSID = _computeCLSID(forPathExtension: "png")! - - /// The `CLSID` value corresponding to the JPEG image format. - /// - /// - Note: The named constant [`ImageFormatJPEG`](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-image-file-format-constants) - /// is not the correct value and will cause `Image::Save()` to fail if - /// passed to it. - private static let _jpegCLSID = _computeCLSID(forPathExtension: "jpg")! - - /// The `CLSID` value corresponding to this image format. + /// The `CLSID` value corresponding to the WIC image encoder for this image + /// format. public var clsid: CLSID { switch kind { case .png: - Self._pngCLSID + CLSID_WICPngEncoder case .jpeg: - Self._jpegCLSID + CLSID_WICJpegEncoder case let .systemValue(clsid): clsid as! CLSID } @@ -219,19 +247,19 @@ extension AttachableImageFormat { /// encoding quality. /// /// - Parameters: - /// - clsid: The `CLSID` value corresponding to the image format to use when - /// encoding images. + /// - clsid: The `CLSID` value corresponding to a WIC image encoder to use + /// when encoding images. /// - encodingQuality: The encoding quality to use when encoding images. For /// the lowest supported quality, pass `0.0`. For the highest supported /// quality, pass `1.0`. /// - /// If the target image format does not support variable-quality encoding, + /// If the target image encoder does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `clsid` does not represent an image format supported by GDI+, the - /// result is undefined. For a list of image formats supported by GDI+, see - /// the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) - /// function. + /// If `clsid` does not represent an image encoder type supported by WIC, the + /// result is undefined. For a list of image encoders supported by WIC, see + /// the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// class. public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) } @@ -249,12 +277,13 @@ extension AttachableImageFormat { /// If the target image format does not support variable-quality encoding, /// the value of the `encodingQuality` argument is ignored. /// - /// If `pathExtension` does not correspond to an image format supported by - /// GDI+, this initializer returns `nil`. For a list of image formats - /// supported by GDI+, see the [GetImageEncoders()](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusimagecodec/nf-gdiplusimagecodec-getimageencoders) - /// function. + /// If `pathExtension` does not correspond to an image format that WIC can use + /// to encode images, this initializer returns `nil`. For a list of image + /// encoders supported by WIC, see the documentation for the [IWICBitmapEncoder](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapencoder) + /// class. public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } + let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) if let clsid { self.init(clsid, encodingQuality: encodingQuality) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift similarity index 90% rename from Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index 54b24d435..ce8f93c42 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -27,10 +27,11 @@ extension Attachment where AttachableValue: ~Copyable { /// attachment. /// /// The following system-provided image types conform to the - /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -53,7 +54,7 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, deinitializeWhenDone: true) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -73,10 +74,11 @@ extension Attachment where AttachableValue: ~Copyable { /// and immediately attaches it to the current test. /// /// The following system-provided image types conform to the - /// ``AttachableAsGDIPlusImage`` protocol and can be attached to a test: + /// ``AttachableAsIWICBitmap`` protocol and can be attached to a test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) /// /// The testing library uses the image format specified by `imageFormat`. Pass /// `nil` to let the testing library decide which image format to use. If you @@ -91,7 +93,7 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, deinitializeWhenDone: false) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift deleted file mode 100644 index 9535cedfd..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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 os(Windows) -@_spi(Experimental) import Testing -internal import _TestingInternals.GDIPlus - -internal import WinSDK - -/// A type describing errors that can be thrown by GDI+. -enum GDIPlusError: Error { - /// A GDI+ status code. - case status(Gdiplus.Status) - - /// The testing library failed to create an in-memory stream. - case streamCreationFailed(HRESULT) - - /// The testing library failed to get an in-memory stream's underlying buffer. - case globalFromStreamFailed(HRESULT) -} - -extension GDIPlusError: CustomStringConvertible { - var description: String { - switch self { - case let .status(status): - "Could not create the corresponding GDI+ image (Gdiplus.Status \(status.rawValue))." - case let .streamCreationFailed(result): - "Could not create an in-memory stream (HRESULT \(result))." - case let .globalFromStreamFailed(result): - "Could not access the buffer containing the encoded image (HRESULT \(result))." - } - } -} - -// MARK: - - -/// Call a function while GDI+ is set up on the current thread. -/// -/// - Parameters: -/// - body: The function to invoke. -/// -/// - Returns: Whatever is returned by `body`. -/// -/// - Throws: Whatever is thrown by `body`. -func withGDIPlus(_ body: () throws -> R) throws -> R { - // "Escape hatch" if the program being tested calls GdiplusStartup() itself in - // some way that is incompatible with our assumptions about it. - if Environment.flag(named: "SWT_GDIPLUS_STARTUP_ENABLED") == false { - return try body() - } - - var token = ULONG_PTR(0) - var input = Gdiplus.GdiplusStartupInput(nil, false, false) - let rStartup = swt_GdiplusStartup(&token, &input, nil) - guard rStartup == Gdiplus.Ok else { - throw GDIPlusError.status(rStartup) - } - defer { - swt_GdiplusShutdown(token) - } - - return try body() -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift deleted file mode 100644 index 466a992ec..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// 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 os(Windows) -import Testing -private import _TestingInternals.GDIPlus - -public import WinSDK - -@_spi(Experimental) -extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { - public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { - swt_GdiplusImageFromHBITMAP(imageAddress, nil) - } - - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - DeleteObject(imageAddress) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..27b25a1eb --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -0,0 +1,34 @@ +// +// 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 os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension HBITMAP__: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer! + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, imageAddress, nil, WICBitmapUsePremultipliedAlpha, &bitmap) + guard rCreate == S_OK, let bitmap else { + throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + } + return bitmap + } + + public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..2648d93ce --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -0,0 +1,34 @@ +// +// 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 os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension HICON__: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + var bitmap: UnsafeMutablePointer! + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHICON(factory, imageAddress, &bitmap) + guard rCreate == S_OK, let bitmap else { + throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + } + return bitmap + } + + public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift new file mode 100644 index 000000000..9e217f9d0 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift @@ -0,0 +1,30 @@ +// +// 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 os(Windows) +import Testing + +public import WinSDK + +@_spi(Experimental) +extension IWICBitmap: _AttachableByAddressAsIWICBitmap { + public static func _copyAttachableIWICBitmap( + from imageAddress: UnsafeMutablePointer, + using factory: UnsafeMutablePointer + ) throws -> UnsafeMutablePointer { + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + return imageAddress + } + + public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { + _ = imageAddress.pointee.lpVtbl.pointee.Release(imageAddress) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift new file mode 100644 index 000000000..00372ef5d --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift @@ -0,0 +1,51 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 os(Windows) +@_spi(Experimental) import Testing + +internal import WinSDK + +/// A type describing errors that can be thrown by WIC or COM while attaching an +/// image. +enum ImageAttachmentError: Error { + /// A call to `QueryInterface()` failed. + case queryInterfaceFailed(Any.Type, HRESULT) + + /// The testing library failed to create a WIC object. + case wicObjectCreationFailed(Any.Type, HRESULT) + + /// An image could not be written. + case imageWritingFailed(HRESULT) + + /// The testing library failed to get an in-memory stream's underlying buffer. + case globalFromStreamFailed(HRESULT) + + /// A property could not be written to a property bag. + case propertyBagWritingFailed(String, HRESULT) +} + +extension ImageAttachmentError: CustomStringConvertible { + var description: String { + switch self { + case let .queryInterfaceFailed(type, result): + "Could not cast a Windows Imaging Component object to type '\(type)' (HRESULT \(result))." + case let .wicObjectCreationFailed(type, result): + "Could not create a Windows Imaging Component object of type '\(type)' (HRESULT \(result))." + case let .imageWritingFailed(result): + "Could not write the image (HRESULT \(result))." + case let .globalFromStreamFailed(result): + "Could not access the buffer containing the encoded image (HRESULT \(result))." + case let .propertyBagWritingFailed(name, result): + "Could not set the property '\(name)' (HRESULT \(result))." + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift deleted file mode 100644 index 5a50ef1e7..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 os(Windows) -import Testing - -@_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { - public func _copyAttachableGDIPlusImage() throws -> OpaquePointer { - try Pointee._copyAttachableGDIPlusImage(at: self) - } - - public func _cleanUpAttachment() { - Pointee._cleanUpAttachment(at: self) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift similarity index 50% rename from Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift rename to Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift index 0269bee56..3e19d01d3 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift @@ -10,18 +10,17 @@ #if os(Windows) import Testing -private import _TestingInternals.GDIPlus public import WinSDK @_spi(Experimental) -extension HICON__: _AttachableByAddressAsGDIPlusImage { - public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { - swt_GdiplusImageFromHICON(imageAddress) +extension UnsafeMutablePointer: AttachableAsIWICBitmap where Pointee: _AttachableByAddressAsIWICBitmap { + public func _copyAttachableIWICBitmap(using factory: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + try Pointee._copyAttachableIWICBitmap(from: self, using: factory) } - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - DeleteObject(imageAddress) + public consuming func _deinitializeAttachment() { + Pointee._deinitializeAttachment(at: self) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 9f23cb140..efaed8220 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -10,7 +10,6 @@ #if os(Windows) @_spi(Experimental) public import Testing -private import _TestingInternals.GDIPlus internal import WinSDK @@ -19,36 +18,38 @@ internal import WinSDK /// /// 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 -/// ``AttachableAsGDIPlusImage``. The following system-provided image types -/// conform to the ``AttachableAsGDIPlusImage`` protocol and can be attached to -/// a test: +/// ``AttachableAsIWICBitmap``. The following system-provided image types +/// conform to the ``AttachableAsIWICBitmap`` protocol and can be attached to a +/// test: /// /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) +/// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) @_spi(Experimental) -public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsGDIPlusImage { +public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsIWICBitmap { /// The underlying image. var image: Image /// The image format to use when encoding the represented image. var imageFormat: AttachableImageFormat? - /// Whether or not to call `_cleanUpAttachment(at:)` on `pointer` when this + /// Whether or not to call `_deinitializeAttachment()` on `image` when this /// instance is deinitialized. /// - /// - Note: If cleanup is not performed, `pointer` is effectively being - /// borrowed from the calling context. - var cleanUpWhenDone: Bool + /// - Note: If deinitialization is not performed and `image` is a type that + /// does not participate in ARC, `image` is effectively being borrowed from + /// the calling context. + private var _deinitializeWhenDone: Bool - init(image: Image, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { + init(image: consuming Image, imageFormat: AttachableImageFormat?, deinitializeWhenDone: Bool) { self.image = image self.imageFormat = imageFormat - self.cleanUpWhenDone = cleanUpWhenDone + self._deinitializeWhenDone = deinitializeWhenDone } deinit { - if cleanUpWhenDone { - image._cleanUpAttachment() + if _deinitializeWhenDone { + image._deinitializeAttachment() } } } @@ -70,40 +71,81 @@ extension _AttachableImageWrapper: AttachableWrapper { var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) guard S_OK == rCreateStream, let stream else { - throw GDIPlusError.streamCreationFailed(rCreateStream) + throw ImageAttachmentError.wicObjectCreationFailed(IStream.self, rCreateStream) } defer { - stream.withMemoryRebound(to: IUnknown.self, capacity: 1) { stream in - _ = swt_IUnknown_Release(stream) - } + _ = stream.pointee.lpVtbl.pointee.Release(stream) } - try withGDIPlus { - // Get a GDI+ image from the attachment. - let image = try image._copyAttachableGDIPlusImage() - defer { - swt_GdiplusImageDelete(image) - } + // Get an imaging factory to create the WIC bitmap and encoder. + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + // Create the bitmap and downcast it to an IWICBitmapSource for later use. + let bitmap = try image.copyAttachableIWICBitmapSource(using: factory) + defer { + _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) + } - // Get the CLSID of the image encoder corresponding to the specified image - // format. - var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) - - var encodingQuality = LONG((imageFormat?.encodingQuality ?? 1.0) * 100.0) - try withUnsafeMutableBytes(of: &encodingQuality) { encodingQuality in - var encoderParams = Gdiplus.EncoderParameters() - encoderParams.Count = 1 - encoderParams.Parameter.Guid = swt_GdiplusEncoderQuality() - encoderParams.Parameter.Type = ULONG(Gdiplus.EncoderParameterValueTypeLong.rawValue) - encoderParams.Parameter.NumberOfValues = 1 - encoderParams.Parameter.Value = encodingQuality.baseAddress - - // Save the image into the stream. - let rSave = swt_GdiplusImageSave(image, stream, &clsid, &encoderParams) - guard rSave == Gdiplus.Ok else { - throw GDIPlusError.status(rSave) - } + // Create the encoder. + let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { IID_IWICBitmapEncoder in + var encoderCLSID = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) + var encoder: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + &encoderCLSID, + nil, + DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + IID_IWICBitmapEncoder, + &encoder + ) + guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { + throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmapEncoder.self, rCreate) } + return encoder + } + defer { + _ = encoder.pointee.lpVtbl.pointee.Release(encoder) + } + _ = encoder.pointee.lpVtbl.pointee.Initialize(encoder, stream, WICBitmapEncoderNoCache) + + // Create the frame into which the bitmap will be composited. + var frame: UnsafeMutablePointer? + var propertyBag: UnsafeMutablePointer? + let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) + guard rCreateFrame == S_OK, let frame, let propertyBag else { + throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) + } + defer { + _ = frame.pointee.lpVtbl.pointee.Release(frame) + _ = propertyBag.pointee.lpVtbl.pointee.Release(propertyBag) + } + + // Set properties. The only property we currently set is image quality. + if let encodingQuality = imageFormat?.encodingQuality { + try propertyBag.write(encodingQuality, named: "ImageQuality") + } + _ = frame.pointee.lpVtbl.pointee.Initialize(frame, propertyBag) + + // Write the image! + let rWrite = frame.pointee.lpVtbl.pointee.WriteSource(frame, bitmap, nil) + guard rWrite == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rWrite) + } + + // Commit changes through the various layers. + var rCommit = frame.pointee.lpVtbl.pointee.Commit(frame) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = encoder.pointee.lpVtbl.pointee.Commit(encoder) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) + } + rCommit = stream.pointee.lpVtbl.pointee.Commit(stream, DWORD(bitPattern: STGC_DEFAULT.rawValue)) + guard rCommit == S_OK else { + throw ImageAttachmentError.imageWritingFailed(rCommit) } // Extract the serialized image and pass it back to the caller. We hold the @@ -112,7 +154,7 @@ extension _AttachableImageWrapper: AttachableWrapper { var global: HGLOBAL? let rGetGlobal = GetHGlobalFromStream(stream, &global) guard S_OK == rGetGlobal else { - throw GDIPlusError.globalFromStreamFailed(rGetGlobal) + throw ImageAttachmentError.globalFromStreamFailed(rGetGlobal) } guard let baseAddress = GlobalLock(global) else { throw Win32Error(rawValue: GetLastError()) diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift new file mode 100644 index 000000000..b3214fca3 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift @@ -0,0 +1,37 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 os(Windows) +internal import WinSDK + +extension UInt128 { + init(_ guid: GUID) { + self = withUnsafeBytes(of: guid) { buffer in + buffer.baseAddress!.loadUnaligned(as: Self.self) + } + } +} + +extension GUID { + init(_ uint128Value: UInt128) { + self = withUnsafeBytes(of: uint128Value) { buffer in + buffer.baseAddress!.loadUnaligned(as: Self.self) + } + } + + static func ==(lhs: Self, rhs: Self) -> Bool { + withUnsafeBytes(of: lhs) { lhs in + withUnsafeBytes(of: rhs) { rhs in + lhs.elementsEqual(rhs) + } + } + } +} +#endif \ No newline at end of file diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift new file mode 100644 index 000000000..4668f967a --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 os(Windows) +internal import WinSDK + +extension UnsafeMutablePointer { + /// Write a floating-point value to this property bag with the given name, + /// + /// - Parameters: + /// - value: The value to write. + /// - propertyName: The name of the property. + /// + /// - Throws: If any error occurred writing the property. + func write(_ value: Float, named propertyName: String) throws { + let rWrite = propertyName.withCString(encodedAs: UTF16.self) { propertyName in + var option = PROPBAG2() + option.pstrName = .init(mutating: propertyName) + + return withUnsafeTemporaryAllocation(of: VARIANT.self, capacity: 1) { variant in + let variant = variant.baseAddress! + VariantInit(variant) + variant.pointee.vt = .init(VT_R4.rawValue) + variant.pointee.fltVal = value + return self.pointee.lpVtbl.pointee.Write(self, 1, &option, variant) + } + } + guard rWrite == S_OK else { + throw ImageAttachmentError.propertyBagWritingFailed(propertyName, rWrite) + } + } +} +#endif \ No newline at end of file diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift new file mode 100644 index 000000000..6c77496aa --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 os(Windows) +internal import WinSDK + +extension IWICImagingFactory { + /// Create an imaging factory. + /// + /// - Returns: A pointer to a new instance of this type. The caller is + /// responsible for releasing this object when done with it. + /// + /// - Throws: Any error that occurred while creating the object. + static func create() throws -> UnsafeMutablePointer { + try withUnsafePointer(to: CLSID_WICImagingFactory) { CLSID_WICImagingFactory in + try withUnsafePointer(to: IID_IWICImagingFactory) { IID_IWICImagingFactory in + var factory: UnsafeMutableRawPointer? + let rCreate = CoCreateInstance( + CLSID_WICImagingFactory, + nil, + DWORD(bitPattern: CLSCTX_INPROC_SERVER.rawValue), + IID_IWICImagingFactory, + &factory + ) + guard rCreate == S_OK, let factory = factory?.assumingMemoryBound(to: Self.self) else { + throw ImageAttachmentError.wicObjectCreationFailed(Self.self, rCreate) + } + return factory + } + } + } +} +#endif \ No newline at end of file diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h deleted file mode 100644 index ff3020b66..000000000 --- a/Sources/_TestingInternals/GDI+/include/GDI+.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 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 !defined(SWT_GDIPLUS_H) -#define SWT_GDIPLUS_H - -/// This header includes thunk functions for various GDI+ functions that the -/// Swift importer is currently unable to import. As such, I haven't documented -/// each function individually; refer to the GDI+ documentation for more -/// information about the thunked functions. - -#if defined(_WIN32) && defined(__cplusplus) -#include "../include/Defines.h" -#include "../include/Includes.h" - -#include - -SWT_ASSUME_NONNULL_BEGIN - -static inline Gdiplus::Status swt_GdiplusStartup( - ULONG_PTR *token, - const Gdiplus::GdiplusStartupInput *input, - Gdiplus::GdiplusStartupOutput *_Nullable output -) { - return Gdiplus::GdiplusStartup(token, input, output); -} - -static inline void swt_GdiplusShutdown(ULONG_PTR token) { - Gdiplus::GdiplusShutdown(token); -} - -static inline Gdiplus::Image *swt_GdiplusImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { - return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); -} - -static inline Gdiplus::Image *swt_GdiplusImageFromHICON(HICON icon) { - return Gdiplus::Bitmap::FromHICON(icon); -} - -static inline Gdiplus::Image *swt_GdiplusImageClone(Gdiplus::Image *image) { - return image->Clone(); -} - -static inline void swt_GdiplusImageDelete(Gdiplus::Image *image) { - delete image; -} - -static inline Gdiplus::Status swt_GdiplusImageSave( - Gdiplus::Image *image, - IStream *stream, - const CLSID *format, - const Gdiplus::EncoderParameters *_Nullable encoderParams -) { - return image->Save(stream, format, encoderParams); -} - -static inline GUID swt_GdiplusEncoderQuality(void) { - return Gdiplus::EncoderQuality; -} - -SWT_ASSUME_NONNULL_END - -#endif -#endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index ae641de0d..636ea9aff 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -108,24 +108,6 @@ static DWORD_PTR swt_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(void) { static const IMAGE_SECTION_HEADER *_Null_unspecified swt_IMAGE_FIRST_SECTION(const IMAGE_NT_HEADERS *ntHeader) { return IMAGE_FIRST_SECTION(ntHeader); } - -#if defined(__cplusplus) -/// Add a reference to (retain) a COM object. -/// -/// This function is provided because `IUnknown::AddRef()` is a virtual member -/// function and cannot be imported directly into Swift. -static inline ULONG swt_IUnknown_AddRef(IUnknown *object) { - return object->AddRef(); -} - -/// Release a COM object. -/// -/// This function is provided because `IUnknown::Release()` is a virtual member -/// function and cannot be imported directly into Swift. -static inline ULONG swt_IUnknown_Release(IUnknown *object) { - return object->Release(); -} -#endif #endif #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__ANDROID__) diff --git a/Sources/_TestingInternals/include/module.modulemap b/Sources/_TestingInternals/include/module.modulemap index 12a23c81d..e05a32552 100644 --- a/Sources/_TestingInternals/include/module.modulemap +++ b/Sources/_TestingInternals/include/module.modulemap @@ -11,11 +11,4 @@ module _TestingInternals { umbrella "." export * - - explicit module GDIPlus { - header "../GDI+/include/GDI+.h" - export * - - link "gdiplus.lib" - } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index d1556ff87..300e8333c 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -752,6 +752,7 @@ extension AttachmentTests { try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) } + Attachment.record(attachment) } @MainActor @Test func attachHBITMAPAsJPEG() throws { @@ -767,20 +768,44 @@ extension AttachmentTests { Attachment.record(loFi) } - @MainActor @Test func pathExtensionAndCLSID() throws { + @MainActor @Test func attachIWICBitmap() throws { + let factory = try IWICImagingFactory.create() + defer { + _ = factory.pointee.lpVtbl.pointee.Release(factory) + } + + let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + + var wicBitmap: UnsafeMutablePointer? + let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, bitmap, nil, WICBitmapUsePremultipliedAlpha, &wicBitmap) + guard rCreate == S_OK, let wicBitmap else { + throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + } + + let attachment = Attachment(wicBitmap, named: "diamond.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + Attachment.record(attachment) + } + + @MainActor @Test func pathExtensionAndCLSID() { let pngCLSID = AttachableImageFormat.png.clsid let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") #expect(pngFilename == "example.png") let jpegCLSID = AttachableImageFormat.jpeg.clsid let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") - #expect(jpegFilename == "example.jpg") + #expect(jpegFilename == "example.jpeg") let pngjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.png") - #expect(pngjpegFilename == "example.png.jpg") + #expect(pngjpegFilename == "example.png.jpeg") - let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpeg") - #expect(jpgjpegFilename == "example.jpeg") + let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpg") + #expect(jpgjpegFilename == "example.jpg") } #endif } From 6431dc597d9ba475e4f1b7a6b1bde20d57d83c02 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 7 Aug 2025 16:54:16 -0400 Subject: [PATCH 2/8] wicObjectCreationFailed -> comObjectCreationFailed --- .../Attachments/AttachableImageFormat+CLSID.swift | 2 +- .../Attachments/HBITMAP+AttachableAsIWICBitmap.swift | 2 +- .../Attachments/HICON+AttachableAsIWICBitmap.swift | 2 +- .../Attachments/ImageAttachmentError.swift | 8 ++++---- .../Attachments/_AttachableImageWrapper.swift | 6 +++--- .../Support/Additions/IWICImagingFactoryAdditions.swift | 4 ++-- Tests/TestingTests/AttachmentTests.swift | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 8323a8096..2fe25abb9 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -32,7 +32,7 @@ extension AttachableImageFormat { &enumerator ) guard rCreate == S_OK, let enumerator else { - throw ImageAttachmentError.wicObjectCreationFailed(IEnumUnknown.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(IEnumUnknown.self, rCreate) } defer { _ = enumerator.pointee.lpVtbl.pointee.Release(enumerator) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift index 27b25a1eb..d5610113c 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -22,7 +22,7 @@ extension HBITMAP__: _AttachableByAddressAsIWICBitmap { var bitmap: UnsafeMutablePointer! let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, imageAddress, nil, WICBitmapUsePremultipliedAlpha, &bitmap) guard rCreate == S_OK, let bitmap else { - throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } return bitmap } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift index 2648d93ce..cf942f1de 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -22,7 +22,7 @@ extension HICON__: _AttachableByAddressAsIWICBitmap { var bitmap: UnsafeMutablePointer! let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHICON(factory, imageAddress, &bitmap) guard rCreate == S_OK, let bitmap else { - throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } return bitmap } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift index 00372ef5d..52a51b708 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/ImageAttachmentError.swift @@ -20,7 +20,7 @@ enum ImageAttachmentError: Error { case queryInterfaceFailed(Any.Type, HRESULT) /// The testing library failed to create a WIC object. - case wicObjectCreationFailed(Any.Type, HRESULT) + case comObjectCreationFailed(Any.Type, HRESULT) /// An image could not be written. case imageWritingFailed(HRESULT) @@ -36,9 +36,9 @@ extension ImageAttachmentError: CustomStringConvertible { var description: String { switch self { case let .queryInterfaceFailed(type, result): - "Could not cast a Windows Imaging Component object to type '\(type)' (HRESULT \(result))." - case let .wicObjectCreationFailed(type, result): - "Could not create a Windows Imaging Component object of type '\(type)' (HRESULT \(result))." + "Could not cast a COM object to type '\(type)' (HRESULT \(result))." + case let .comObjectCreationFailed(type, result): + "Could not create a COM object of type '\(type)' (HRESULT \(result))." case let .imageWritingFailed(result): "Could not write the image (HRESULT \(result))." case let .globalFromStreamFailed(result): diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index efaed8220..ca8ee04f0 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -71,7 +71,7 @@ extension _AttachableImageWrapper: AttachableWrapper { var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) guard S_OK == rCreateStream, let stream else { - throw ImageAttachmentError.wicObjectCreationFailed(IStream.self, rCreateStream) + throw ImageAttachmentError.comObjectCreationFailed(IStream.self, rCreateStream) } defer { _ = stream.pointee.lpVtbl.pointee.Release(stream) @@ -101,7 +101,7 @@ extension _AttachableImageWrapper: AttachableWrapper { &encoder ) guard rCreate == S_OK, let encoder = encoder?.assumingMemoryBound(to: IWICBitmapEncoder.self) else { - throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmapEncoder.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapEncoder.self, rCreate) } return encoder } @@ -115,7 +115,7 @@ extension _AttachableImageWrapper: AttachableWrapper { var propertyBag: UnsafeMutablePointer? let rCreateFrame = encoder.pointee.lpVtbl.pointee.CreateNewFrame(encoder, &frame, &propertyBag) guard rCreateFrame == S_OK, let frame, let propertyBag else { - throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmapFrameEncode.self, rCreateFrame) } defer { _ = frame.pointee.lpVtbl.pointee.Release(frame) diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift index 6c77496aa..dc11ab0fc 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IWICImagingFactoryAdditions.swift @@ -30,11 +30,11 @@ extension IWICImagingFactory { &factory ) guard rCreate == S_OK, let factory = factory?.assumingMemoryBound(to: Self.self) else { - throw ImageAttachmentError.wicObjectCreationFailed(Self.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(Self.self, rCreate) } return factory } } } } -#endif \ No newline at end of file +#endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 300e8333c..dc01358b1 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -782,7 +782,7 @@ extension AttachmentTests { var wicBitmap: UnsafeMutablePointer? let rCreate = factory.pointee.lpVtbl.pointee.CreateBitmapFromHBITMAP(factory, bitmap, nil, WICBitmapUsePremultipliedAlpha, &wicBitmap) guard rCreate == S_OK, let wicBitmap else { - throw ImageAttachmentError.wicObjectCreationFailed(IWICBitmap.self, rCreate) + throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } let attachment = Attachment(wicBitmap, named: "diamond.png") From 5ea7108b140159094ded927bf7d245fbe5043c3d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 7 Aug 2025 17:13:42 -0400 Subject: [PATCH 3/8] Trailing newlines --- .../_Testing_WinSDK/Support/Additions/GUIDAdditions.swift | 2 +- .../Support/Additions/IPropertyBag2Additions.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift index b3214fca3..c58eca577 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/GUIDAdditions.swift @@ -34,4 +34,4 @@ extension GUID { } } } -#endif \ No newline at end of file +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift index 4668f967a..307e25778 100644 --- a/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift +++ b/Sources/Overlays/_Testing_WinSDK/Support/Additions/IPropertyBag2Additions.swift @@ -37,4 +37,4 @@ extension UnsafeMutablePointer { } } } -#endif \ No newline at end of file +#endif From 644fec239a951fabaf62c00a96da6ae4d3c36ad4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 12:21:09 -0400 Subject: [PATCH 4/8] Make sure we copy HBITMAP and HICON values when attaching (avoid unsafe consumption pattern) --- .../Attachments/AttachableAsIWICBitmap.swift | 49 +++++++++++++++++-- .../Attachment+AttachableAsIWICBitmap.swift | 11 +---- .../HBITMAP+AttachableAsIWICBitmap.swift | 10 +++- .../HICON+AttachableAsIWICBitmap.swift | 12 ++++- .../IWICBitmap+AttachableAsIWICBitmap.swift | 7 ++- ...utablePointer+AttachableAsIWICBitmap.swift | 8 ++- .../Attachments/_AttachableImageWrapper.swift | 37 +++++++------- Tests/TestingTests/AttachmentTests.swift | 22 ++++++--- 8 files changed, 111 insertions(+), 45 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift index c50e0fa36..416048650 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsIWICBitmap.swift @@ -53,21 +53,44 @@ public protocol _AttachableByAddressAsIWICBitmap { using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer + /// Make a copy of the instance of this type at the given address. + /// + /// - Parameters: + /// - imageAddress: The address of the instance of this type that should be + /// copied. + /// + /// - Returns: A copy of `imageAddress`, or `imageAddress` if this type does + /// not support a copying operation. + /// + /// - Throws: Any error that prevented copying the value at `imageAddress`. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to the value at `imageAddress`. If this type does not support + /// making copies, return `imageAddress` verbatim. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer + /// Manually deinitialize any resources at the given address. /// /// - Parameters: /// - imageAddress: The address of the instance of this type. /// - /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) associated with this image. The testing library - /// automatically invokes this function as needed. + /// The implementation of this function is responsible for balancing a + /// previous call to `_copyAttachableValue(at:)` by cleaning up any resources + /// (such as handles or COM objects) associated with the value at + /// `imageAddress`. The testing library automatically invokes this function as + /// needed. If `_copyAttachableValue(at:)` threw an error, the testing library + /// does not call this function. /// /// This function is not responsible for releasing the image returned from /// `_copyAttachableIWICBitmap(from:using:)`. /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - static func _deinitializeAttachment(at imageAddress: consuming UnsafeMutablePointer) + static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) } /// A protocol describing images that can be converted to instances of @@ -107,6 +130,22 @@ public protocol AttachableAsIWICBitmap { using factory: UnsafeMutablePointer ) throws -> UnsafeMutablePointer + /// Make a copy of this instance. + /// + /// - Returns: A copy of `self`, or `self` if this type does not support a + /// copying operation. + /// + /// - Throws: Any error that prevented copying this value. + /// + /// The testing library uses this function to take ownership of image + /// resources that test authors pass to it. If possible, make a copy of or add + /// a reference to `self`. If this type does not support making copies, return + /// `self` verbatim. + /// + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. + func _copyAttachableValue() throws -> Self + /// Manually deinitialize any resources associated with this image. /// /// The implementation of this function cleans up any resources (such as @@ -118,7 +157,7 @@ public protocol AttachableAsIWICBitmap { /// /// This function is not part of the public interface of the testing library. /// It may be removed in a future update. - consuming func _deinitializeAttachment() + func _deinitializeAttachableValue() } extension AttachableAsIWICBitmap { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index ce8f93c42..eeb56039e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -40,16 +40,9 @@ extension Attachment where AttachableValue: ~Copyable { /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. - /// - /// - Important: The resulting instance of ``Attachment`` takes ownership of - /// `attachableValue` and frees its resources upon deinitialization. If you - /// do not want the testing library to take ownership of this value, call - /// ``Attachment/record(_:named:as:sourceLocation)`` instead of this - /// initializer, or make a copy of the resource before passing it to this - /// initializer. @unsafe public init( - _ attachableValue: consuming T, + _ attachableValue: borrowing T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation @@ -93,7 +86,7 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, deinitializeWhenDone: false) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat, deinitializeWhenDone: false) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift index d5610113c..7223e3cf2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsIWICBitmap.swift @@ -27,7 +27,15 @@ extension HBITMAP__: _AttachableByAddressAsIWICBitmap { return bitmap } - public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + let result: HBITMAP? = CopyImage(imageAddress, UINT(IMAGE_BITMAP), 0, 0, 0).assumingMemoryBound(to: Self.self) + guard let result else { + throw Win32Error(rawValue: GetLastError()) + } + return result + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { DeleteObject(imageAddress) } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift index cf942f1de..0a882a38c 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -27,8 +27,16 @@ extension HICON__: _AttachableByAddressAsIWICBitmap { return bitmap } - public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { - DeleteObject(imageAddress) + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + let result: HICON? = CopyImage(imageAddress, UINT(IMAGE_ICON), 0, 0, 0).assumingMemoryBound(to: Self.self) + guard let result else { + throw Win32Error(rawValue: GetLastError()) + } + return result + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { + DestroyIcon(imageAddress) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift index 9e217f9d0..a7419e3fd 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/IWICBitmap+AttachableAsIWICBitmap.swift @@ -23,7 +23,12 @@ extension IWICBitmap: _AttachableByAddressAsIWICBitmap { return imageAddress } - public static func _deinitializeAttachment(at imageAddress: UnsafeMutablePointer) { + public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { + _ = imageAddress.pointee.lpVtbl.pointee.AddRef(imageAddress) + return imageAddress + } + + public static func _deinitializeAttachableValue(at imageAddress: UnsafeMutablePointer) { _ = imageAddress.pointee.lpVtbl.pointee.Release(imageAddress) } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift index 3e19d01d3..1111fd178 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsIWICBitmap.swift @@ -19,8 +19,12 @@ extension UnsafeMutablePointer: AttachableAsIWICBitmap where Pointee: _Attachabl try Pointee._copyAttachableIWICBitmap(from: self, using: factory) } - public consuming func _deinitializeAttachment() { - Pointee._deinitializeAttachment(at: self) + public func _copyAttachableValue() throws -> Self { + try Pointee._copyAttachableValue(at: self) + } + + public consuming func _deinitializeAttachableValue() { + Pointee._deinitializeAttachableValue(at: self) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index ca8ee04f0..6ccc804a4 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -26,45 +26,44 @@ internal import WinSDK /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) /// - [`IWICBitmap`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmap) @_spi(Experimental) -public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsIWICBitmap { +public final class _AttachableImageWrapper: Sendable where Image: AttachableAsIWICBitmap { /// The underlying image. - var image: Image + nonisolated(unsafe) let image: Result /// The image format to use when encoding the represented image. - var imageFormat: AttachableImageFormat? + let imageFormat: AttachableImageFormat? - /// Whether or not to call `_deinitializeAttachment()` on `image` when this + /// Whether or not to call `_deinitializeAttachableValue()` on `image` when this /// instance is deinitialized. /// /// - Note: If deinitialization is not performed and `image` is a type that /// does not participate in ARC, `image` is effectively being borrowed from /// the calling context. - private var _deinitializeWhenDone: Bool + private let _deinitializeWhenDone: Bool - init(image: consuming Image, imageFormat: AttachableImageFormat?, deinitializeWhenDone: Bool) { - self.image = image + init(image: borrowing Image, imageFormat: AttachableImageFormat?, deinitializeWhenDone: Bool) { + self.image = Result { [image = copy image] in + try image._copyAttachableValue() + } self.imageFormat = imageFormat self._deinitializeWhenDone = deinitializeWhenDone } deinit { - if _deinitializeWhenDone { - image._deinitializeAttachment() + if _deinitializeWhenDone, let image = try? image.get() { + image._deinitializeAttachableValue() } } } -@available(*, unavailable) -extension _AttachableImageWrapper: Sendable {} - // MARK: - extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image + public var wrappedValue: Image? { + try? image.get() } - public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + public func withUnsafeBytes(for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { // Create an in-memory stream to write the image data to. Note that Windows // documentation recommends SHCreateMemStream() instead, but that function // does not provide a mechanism to access the underlying memory directly. @@ -84,14 +83,14 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Create the bitmap and downcast it to an IWICBitmapSource for later use. - let bitmap = try image.copyAttachableIWICBitmapSource(using: factory) + let bitmap = try image.get().copyAttachableIWICBitmapSource(using: factory) defer { _ = bitmap.pointee.lpVtbl.pointee.Release(bitmap) } // Create the encoder. - let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { IID_IWICBitmapEncoder in - var encoderCLSID = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) + let encoder = try withUnsafePointer(to: IID_IWICBitmapEncoder) { [preferredName = attachment.preferredName] IID_IWICBitmapEncoder in + var encoderCLSID = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: preferredName) var encoder: UnsafeMutableRawPointer? let rCreate = CoCreateInstance( &encoderCLSID, @@ -166,7 +165,7 @@ extension _AttachableImageWrapper: AttachableWrapper { return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) } - public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + public borrowing func preferredName(for attachment: borrowing Attachment<_AttachableImageWrapper>, basedOn suggestedName: String) -> String { let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index dc01358b1..8604e05c3 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -709,7 +709,7 @@ extension AttachmentTests { @MainActor @Test func attachHICON() throws { let icon = try copyHICON() defer { - DeleteObject(icon) + DestroyIcon(icon) } let attachment = Attachment(icon, named: "diamond.jpeg") @@ -723,7 +723,7 @@ extension AttachmentTests { let icon = try copyHICON() defer { - DeleteObject(icon) + DestroyIcon(icon) } let screenDC = try #require(GetDC(nil)) @@ -748,6 +748,10 @@ extension AttachmentTests { @MainActor @Test func attachHBITMAP() throws { let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + let attachment = Attachment(bitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) @@ -756,10 +760,13 @@ extension AttachmentTests { } @MainActor @Test func attachHBITMAPAsJPEG() throws { - let bitmap1 = try copyHBITMAP() - let hiFi = Attachment(bitmap1, named: "diamond", as: .jpeg(withEncodingQuality: 1.0)) - let bitmap2 = try copyHBITMAP() - let loFi = Attachment(bitmap2, named: "diamond", as: .jpeg(withEncodingQuality: 0.1)) + let bitmap = try copyHBITMAP() + defer { + DeleteObject(bitmap) + } + let hiFi = Attachment(bitmap, named: "hifi", as: .jpeg(withEncodingQuality: 1.0)) + let loFi = Attachment(bitmap, named: "lofi", as: .jpeg(withEncodingQuality: 0.1)) + try hiFi.withUnsafeBytes { hiFi in try loFi.withUnsafeBytes { loFi in #expect(hiFi.count > loFi.count) @@ -784,6 +791,9 @@ extension AttachmentTests { guard rCreate == S_OK, let wicBitmap else { throw ImageAttachmentError.comObjectCreationFailed(IWICBitmap.self, rCreate) } + defer { + _ = wicBitmap.pointee.lpVtbl.pointee.Release(wicBitmap) + } let attachment = Attachment(wicBitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in From 3c2365acf2dd238c0623acfe3455ce484a33b4e1 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 13:01:56 -0400 Subject: [PATCH 5/8] Simplify HICON copy --- .../Attachments/HICON+AttachableAsIWICBitmap.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift index 0a882a38c..e06b51388 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsIWICBitmap.swift @@ -28,8 +28,7 @@ extension HICON__: _AttachableByAddressAsIWICBitmap { } public static func _copyAttachableValue(at imageAddress: UnsafeMutablePointer) throws -> UnsafeMutablePointer { - let result: HICON? = CopyImage(imageAddress, UINT(IMAGE_ICON), 0, 0, 0).assumingMemoryBound(to: Self.self) - guard let result else { + guard let result = CopyIcon(imageAddress) else { throw Win32Error(rawValue: GetLastError()) } return result From 8c79596709634c8b6c531fc04165423c922d27ed Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 13:17:36 -0400 Subject: [PATCH 6/8] Remove deinitializeWhenDone (not needed anymore) --- .../Attachment+AttachableAsIWICBitmap.swift | 4 ++-- .../Attachments/_AttachableImageWrapper.swift | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index eeb56039e..089d896da 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -47,7 +47,7 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, deinitializeWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -86,7 +86,7 @@ extension Attachment where AttachableValue: ~Copyable { as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat, deinitializeWhenDone: false) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 6ccc804a4..72afbb575 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -33,24 +33,15 @@ public final class _AttachableImageWrapper: Sendable where Image: Attacha /// The image format to use when encoding the represented image. let imageFormat: AttachableImageFormat? - /// Whether or not to call `_deinitializeAttachableValue()` on `image` when this - /// instance is deinitialized. - /// - /// - Note: If deinitialization is not performed and `image` is a type that - /// does not participate in ARC, `image` is effectively being borrowed from - /// the calling context. - private let _deinitializeWhenDone: Bool - - init(image: borrowing Image, imageFormat: AttachableImageFormat?, deinitializeWhenDone: Bool) { + init(image: borrowing Image, imageFormat: AttachableImageFormat?) { self.image = Result { [image = copy image] in try image._copyAttachableValue() } self.imageFormat = imageFormat - self._deinitializeWhenDone = deinitializeWhenDone } deinit { - if _deinitializeWhenDone, let image = try? image.get() { + if let image = try? image.get() { image._deinitializeAttachableValue() } } From 94f0ba8ff8ea5c6a93e22e8a2014a90bca1aadd3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 13:32:31 -0400 Subject: [PATCH 7/8] Remove @unsafe from init() as we no longer need to worry about a dangling HBITMAP reference --- .../Attachments/Attachment+AttachableAsIWICBitmap.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index 089d896da..0172c6722 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -40,7 +40,6 @@ extension Attachment where AttachableValue: ~Copyable { /// specify a path extension, or if the path extension you specify doesn't /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. - @unsafe public init( _ attachableValue: borrowing T, named preferredName: String? = nil, From 36c99457be62659c5ce0feb4952139804e5b9dc7 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 8 Aug 2025 13:58:19 -0400 Subject: [PATCH 8/8] Consistently call the arg to Attachment 'image' rather than 'attachableValue', conditionally expose an imageFormat property on Attachment --- .../Attachment+AttachableAsCGImage.swift | 22 ++++++++++++++----- .../Attachment+AttachableAsIWICBitmap.swift | 22 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift index 10c866e3a..866240e64 100644 --- a/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift +++ b/Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift @@ -17,12 +17,11 @@ 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. + /// - image: 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. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -45,12 +44,12 @@ extension Attachment { /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. public init( - _ attachableValue: T, + _ image: T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -61,7 +60,7 @@ extension Attachment { /// - 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. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this function. /// /// This function creates a new instance of ``Attachment`` wrapping `image` @@ -94,4 +93,15 @@ extension Attachment { Self.record(attachment, sourceLocation: sourceLocation) } } + +@_spi(Experimental) // STOP: not part of ST-0014 +@available(_uttypesAPI, *) +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsCGImage { + /// The image format to use when encoding the represented image. + @_disfavoredOverload + public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + (attachableValue as? _AttachableImageWrapper)?.imageFormat + } +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift index 0172c6722..b8ff6552e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsIWICBitmap.swift @@ -16,12 +16,12 @@ extension Attachment where AttachableValue: ~Copyable { /// Initialize an instance of this type that encloses the given image. /// /// - Parameters: - /// - attachableValue: A pointer to the value that will be attached to the - /// output of the test run. + /// - image: A pointer to 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. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -41,12 +41,12 @@ extension Attachment where AttachableValue: ~Copyable { /// correspond to an image format the operating system knows how to write, the /// testing library selects an appropriate image format for you. public init( - _ attachableValue: borrowing T, + _ image: borrowing T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) + let imageWrapper = _AttachableImageWrapper(image: image, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -57,7 +57,7 @@ extension Attachment where AttachableValue: ~Copyable { /// - 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. - /// - imageFormat: The image format with which to encode `attachableValue`. + /// - imageFormat: The image format with which to encode `image`. /// - sourceLocation: The source location of the call to this initializer. /// This value is used when recording issues associated with the /// attachment. @@ -90,4 +90,14 @@ extension Attachment where AttachableValue: ~Copyable { Self.record(attachment, sourceLocation: sourceLocation) } } + +@_spi(Experimental) +extension Attachment where AttachableValue: AttachableWrapper, AttachableValue.Wrapped: AttachableAsIWICBitmap { + /// The image format to use when encoding the represented image. + @_disfavoredOverload + public var imageFormat: AttachableImageFormat? { + // FIXME: no way to express `where AttachableValue == _AttachableImageWrapper` on a property + (attachableValue as? _AttachableImageWrapper)?.imageFormat + } +} #endif