From 03d8390433a30b7edf6b9bdf18b3d7e10b5c0292 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 9 Jul 2025 16:04:23 -0400 Subject: [PATCH 01/25] [WIP] GDI+ Image overlay --- Package.swift | 11 +++ .../AttachableAsGDIPlusImage.swift | 24 +++++++ .../_Testing_WinSDK/Attachments/GDI+.swift | 27 +++++++ .../HBITMAP+AttachableAsGDIPlusImage.swift | 70 +++++++++++++++++++ .../Attachments/_AttachableImageWrapper.swift | 45 ++++++++++++ .../_Testing_WinSDK/ReexportTesting.swift | 12 ++++ Sources/_TestingInternals/GDI+.cpp | 59 ++++++++++++++++ Sources/_TestingInternals/include/GDI+.h | 31 ++++++++ 8 files changed, 279 insertions(+) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift create mode 100644 Sources/_TestingInternals/GDI+.cpp create mode 100644 Sources/_TestingInternals/include/GDI+.h diff --git a/Package.swift b/Package.swift index 3405be19d..8a51da4dc 100644 --- a/Package.swift +++ b/Package.swift @@ -92,6 +92,7 @@ let package = Package( "_Testing_CoreGraphics", "_Testing_CoreImage", "_Testing_UIKit", + "_Testing_WinSDK", ] ) ] @@ -142,6 +143,7 @@ let package = Package( "_Testing_CoreImage", "_Testing_Foundation", "_Testing_UIKit", + "_Testing_WinSDK", "MemorySafeTestingTests", ], swiftSettings: .packageSettings @@ -253,6 +255,15 @@ let package = Package( path: "Sources/Overlays/_Testing_UIKit", swiftSettings: .packageSettings + .enableLibraryEvolution() ), + .target( + name: "_Testing_WinSDK", + dependencies: [ + "Testing", + "_Testing_WinSDKInternals", + ], + path: "Sources/Overlays/_Testing_WinSDK", + swiftSettings: .packageSettings + .enableLibraryEvolution() + ), // Utility targets: These are utilities intended for use when developing // this package, not for distribution. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..c58eaceda --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -0,0 +1,24 @@ +// +// 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) +//public import WinSDK +//private import _Testing_WinSDKInternals + +public protocol AttachableAsGDIPlusImage { + /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) + /// by design. The caller is responsible for guarding against concurrent + /// access to the resulting GDI+ image object. + func _withGDIPlusImage( + for attachment: Attachable>, + _ body: (UnsafeMutableRawPointer) throws -> R + ) throws -> R +} +//#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift new file mode 100644 index 000000000..8d6a23006 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -0,0 +1,27 @@ +// +// 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) +struct GDIPlusError: Error, RawRepresentable { + var rawValue: CInt +} + +func withGDIPlus(_ body: () throws -> R) throws -> R { + var error = CInt(0) + guard let token = swt_gdiplus_startup(&error) else { + throw GDIPlusError(rawValue: error) + } + defer { + swt_gdiplus_shutdown(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 new file mode 100644 index 000000000..e1040795a --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -0,0 +1,70 @@ +// +// 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) +public import WinSDK +private import _Testing_Internals + +public final class _AttachableHBITMAPWrapper { + private let _bitmap: HBITMAP + private let _palette: HPALETTE? + + init(bitmap: consuming HBITMAP, palette: consuming HPALETTE) { + _bitmap = bitmap + _palette = palette + } + + deinit { + DeleteObject(_bitmap) + if let _palette { + DeleteObject(_palette) + } + } +} + +extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { + public func _withGDIPlusImage( + for attachment: Attachable>, + _ body: (UnsafeMutableRawPointer) throws -> R + ) throws -> R { + let image = swt_gdiplus_createImageFromHBITMAP(_bitmap, _palette) + defer { + swt_gdiplus_destroyImage(image) + } + return try withExtendedLifetime(self) { + try body(image) + } + } +} + +extension Attachment where AttachableValue == _AttachableImageWrapper<_AttachableHBITMAPWrapper> { + public init( + _ bitmap: consuming HBITMAP, + with palette: consuming HPALETTE? = nil, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) { + let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) + self.init(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) + } + + public static func record( + _ bitmap: consuming HBITMAP, + with palette: consuming HPALETTE? = nil, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) { + let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) + Self.record(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift new file mode 100644 index 000000000..5a3797a08 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -0,0 +1,45 @@ +// +// 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) +private import _TestingInternals + +public struct _AttachableImageWrapper: Sendable where Image: AttachableAsGDIPlusImage { + /// The underlying image. + /// + /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` + /// instances can be created from closures that are run at rendering time. + /// The AppKit cross-import overlay is responsible for ensuring that any + /// instances of this type it creates hold "safe" `NSImage` instances. + nonisolated(unsafe) var image: Image + + /// The image format to use when encoding the represented image. + var imageFormat: AttachableImageFormat? + + init(image: Image, imageFormat: AttachableImageFormat?) { + self.image = image._makeCopyForAttachment() + self.imageFormat = imageFormat + } +} + +extension _AttachableImageWrapper: AttachableWrapper { + public var wrappedValue: Image { + image + } + + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + try withGDIPlus { + try image._withGDIPlusImage(for: attachment) { image in + fatalError("GDI+ Unimplemented \(#function)") + } + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift new file mode 100644 index 000000000..ce80a70d9 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift @@ -0,0 +1,12 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing +@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics diff --git a/Sources/_TestingInternals/GDI+.cpp b/Sources/_TestingInternals/GDI+.cpp new file mode 100644 index 000000000..1c003cc8b --- /dev/null +++ b/Sources/_TestingInternals/GDI+.cpp @@ -0,0 +1,59 @@ +// +// 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 +// + +#include "GDI+.h" + +#include +#include + +using Gdiplus; + +ULONG_PTR swt_gdiplus_startup(int *outError) { + ULONG_PTR result = nullptr; + + GdiplusStartupInput input; + auto status = GdiplusStartup(&result, &input, nullptr); + + if (status != Ok) { + *outError = static_cast(status); + } + return result; +} + +void swt_gdiplus_shutdown(ULONG_PTR *token) { + (void)GdiplusShutdown(token); +} + +void *swt_gdiplus_createImageFromHBITMAP(HBITMAP bitmap, HPALETTE palette) { + return Bitmap::FromHBITMAP(bitmap, palette); +} + +void swt_gdiplus_destroyImage(void *image) { + auto bitmap = reinterpret_cast(image); + delete bitmap; +} + +void *swt_gdiplus_copyBytes(void *image, const CLSID *clsid, size_t *outByteCount, int *outError) { + auto bitmap = reinterpret_cast(image); + + // Create an IStream in memory and save the image to it. + auto stream = SHCreateMemStream(nullptr, 0); + auto status = bitmap->Save(stream, clsid); + + // Read back from the stream into + (void)stream->Seek(0, STREAM_SEEK_SET, nullptr); + + stream->Release(); + + if (status != Ok) { + *outError = static_cast(status); + } + return nullptr; +} diff --git a/Sources/_TestingInternals/include/GDI+.h b/Sources/_TestingInternals/include/GDI+.h new file mode 100644 index 000000000..27be026c9 --- /dev/null +++ b/Sources/_TestingInternals/include/GDI+.h @@ -0,0 +1,31 @@ +// +// 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 + +#if defined(_WIN32) + +#include "Defines.h" +#include "Includes.h" + +SWT_ASSUME_NONNULL_BEGIN +SWT_EXTERN ULONG_PTR swt_gdiplus_startup(int *outError); +SWT_EXTERN void swt_gdiplus_shutdown(ULONG_PTR *token); + +SWT_EXTERN void *swt_gdiplus_createImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette); +SWT_EXTERN void swt_gdiplus_destroyImage(void *image); + +SWT_EXTERN void *_Nullable swt_gdiplus_copyBytes(void *image, const CLSID *clsid, size_t *outByteCount, int *outError); +SWT_ASSUME_NONNULL_END + +#endif + +#endif // SWT_DEFINES_H From bfcf2e01f6bc5fb77a4848f8c00d6f785b8688ac Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 15:20:18 -0400 Subject: [PATCH 02/25] More work --- Package.swift | 21 ++++++- .../AttachableAsGDIPlusImage.swift | 27 +++++++-- .../_Testing_WinSDK/Attachments/GDI+.swift | 20 +++++-- .../HBITMAP+AttachableAsGDIPlusImage.swift | 15 +++-- .../Attachments/_AttachableImageWrapper.swift | 45 -------------- .../_Testing_WinSDK/ReexportTesting.swift | 3 +- Sources/_Gdiplus/include/Includes.h | 32 ++++++++++ Sources/_Gdiplus/include/module.modulemap | 14 +++++ Sources/_TestingInternals/GDI+.cpp | 59 ------------------- Sources/_TestingInternals/include/GDI+.h | 31 ---------- Sources/_TestingInternals/include/Includes.h | 1 - 11 files changed, 107 insertions(+), 161 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift create mode 100644 Sources/_Gdiplus/include/Includes.h create mode 100644 Sources/_Gdiplus/include/module.modulemap delete mode 100644 Sources/_TestingInternals/GDI+.cpp delete mode 100644 Sources/_TestingInternals/include/GDI+.h diff --git a/Package.swift b/Package.swift index 5c7f56b5f..e2f1a9672 100644 --- a/Package.swift +++ b/Package.swift @@ -252,10 +252,16 @@ let package = Package( name: "_Testing_WinSDK", dependencies: [ "Testing", - "_Testing_WinSDKInternals", - ], + ] + { +#if os(Windows) + ["_Gdiplus"] +#else + [] +#endif + }(), path: "Sources/Overlays/_Testing_WinSDK", - swiftSettings: .packageSettings + .enableLibraryEvolution() + swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)], + linkerSettings: [.linkedLibrary("Gdiplus.lib", .when(platforms: [.windows]))] ), // Utility targets: These are utilities intended for use when developing @@ -286,6 +292,15 @@ package.targets.append(contentsOf: [ ]) #endif +#if os(Windows) +package.targets.append(contentsOf: [ + .target( + name: "_Gdiplus", + cxxSettings: .packageSettings + ), +]) +#endif + extension BuildSettingCondition { /// Creates a build setting condition that evaluates to `true` for Embedded /// Swift. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index c58eaceda..733c6a90a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -8,17 +8,32 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -//#if os(Windows) -//public import WinSDK -//private import _Testing_WinSDKInternals +#if os(Windows) +private import _Gdiplus public protocol AttachableAsGDIPlusImage { /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. + /// + /// - Warning: Do not call this function directly. Instead, call ``withGDIPlusImage(for:_:)``. func _withGDIPlusImage( - for attachment: Attachable>, - _ body: (UnsafeMutableRawPointer) throws -> R + for attachment: borrowing Attachment>, + _ body: (UnsafeRawPointer) throws -> R ) throws -> R } -//#endif + +extension AttachableAsGDIPlusImage { + /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) + /// by design. The caller is responsible for guarding against concurrent + /// access to the resulting GDI+ image object. + public func withGDIPlusImage( + for attachment: borrowing Attachment>, + _ body: (UnsafeRawPointer) throws -> R + ) throws -> R { + try withGDIPlus(for: attachment) { attachment in + try self._withGDIPlusImage(for: attachment, body) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index 8d6a23006..f7082bf09 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -9,19 +9,27 @@ // #if os(Windows) +internal import WinSDK +private import _Gdiplus + struct GDIPlusError: Error, RawRepresentable { var rawValue: CInt } -func withGDIPlus(_ body: () throws -> R) throws -> R { - var error = CInt(0) - guard let token = swt_gdiplus_startup(&error) else { - throw GDIPlusError(rawValue: error) +func withGDIPlus( + for attachmenthment: borrowing Attachment, + _ body: (borrowing Attachment) throws -> R +) throws -> R where A: ~Copyable { + var token = ULONG_PTR(0) + var input = Gdiplus.GdiplusStartupInput(nil, false, false) + let rStartup = swt_winsdk_GdiplusStartup(&token, &input, nil) + guard rStartup == Gdiplus.Ok else { + throw GDIPlusError(rawValue: rStartup.rawValue) } defer { - swt_gdiplus_shutdown(token) + swt_winsdk_GdiplusShutdown(token) } - return try body() + return try body(attachmenthment) } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index e1040795a..c5ff52aa9 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -9,14 +9,16 @@ // #if os(Windows) +public import Testing + public import WinSDK -private import _Testing_Internals +private import _Gdiplus public final class _AttachableHBITMAPWrapper { private let _bitmap: HBITMAP private let _palette: HPALETTE? - init(bitmap: consuming HBITMAP, palette: consuming HPALETTE) { + init(bitmap: consuming HBITMAP, palette: consuming HPALETTE? = nil) { _bitmap = bitmap _palette = palette } @@ -34,10 +36,7 @@ extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { for attachment: Attachable>, _ body: (UnsafeMutableRawPointer) throws -> R ) throws -> R { - let image = swt_gdiplus_createImageFromHBITMAP(_bitmap, _palette) - defer { - swt_gdiplus_destroyImage(image) - } + let image = swt_winsdk_GdiplusBitmapCreate(_bitmap, _palette) return try withExtendedLifetime(self) { try body(image) } @@ -46,8 +45,8 @@ extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { extension Attachment where AttachableValue == _AttachableImageWrapper<_AttachableHBITMAPWrapper> { public init( - _ bitmap: consuming HBITMAP, - with palette: consuming HPALETTE? = nil, + _ bitmap: consuming WinSDK.HBITMAP, + with palette: consuming WinSDK.HPALETTE? = nil, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift deleted file mode 100644 index 5a3797a08..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ /dev/null @@ -1,45 +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) -private import _TestingInternals - -public struct _AttachableImageWrapper: Sendable where Image: AttachableAsGDIPlusImage { - /// The underlying image. - /// - /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` - /// instances can be created from closures that are run at rendering time. - /// The AppKit cross-import overlay is responsible for ensuring that any - /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) var image: Image - - /// The image format to use when encoding the represented image. - var imageFormat: AttachableImageFormat? - - init(image: Image, imageFormat: AttachableImageFormat?) { - self.image = image._makeCopyForAttachment() - self.imageFormat = imageFormat - } -} - -extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image - } - - public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { - try withGDIPlus { - try image._withGDIPlusImage(for: attachment) { image in - fatalError("GDI+ Unimplemented \(#function)") - } - } - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift index ce80a70d9..48dff4164 100644 --- a/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift +++ b/Sources/Overlays/_Testing_WinSDK/ReexportTesting.swift @@ -1,7 +1,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// 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 @@ -9,4 +9,3 @@ // @_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import Testing -@_exported @_spi(Experimental) @_spi(ForToolsIntegrationOnly) public import _Testing_CoreGraphics diff --git a/Sources/_Gdiplus/include/Includes.h b/Sources/_Gdiplus/include/Includes.h new file mode 100644 index 000000000..be40c5f8c --- /dev/null +++ b/Sources/_Gdiplus/include/Includes.h @@ -0,0 +1,32 @@ +// +// 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_INCLUDES_H) +#define SWT_GDIPLUS_INCLUDES_H + +#include +#include + +static inline Gdiplus::Status swt_winsdk_GdiplusStartup( + ULONG_PTR *token, + const Gdiplus::GdiplusStartupInput *input, + Gdiplus::GdiplusStartupOutput *output +) { + return Gdiplus::GdiplusStartup(token, input, output); +} + +static inline void swt_winsdk_GdiplusShutdown(ULONG_PTR token) { + Gdiplus::GdiplusShutdown(token); +} + +static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HBITMAP bitmap, HPALETTE palette) { + return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); +} +#endif diff --git a/Sources/_Gdiplus/include/module.modulemap b/Sources/_Gdiplus/include/module.modulemap new file mode 100644 index 000000000..73022d14a --- /dev/null +++ b/Sources/_Gdiplus/include/module.modulemap @@ -0,0 +1,14 @@ +// +// 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 +// + +module _Gdiplus { + umbrella "." + export * +} diff --git a/Sources/_TestingInternals/GDI+.cpp b/Sources/_TestingInternals/GDI+.cpp deleted file mode 100644 index 1c003cc8b..000000000 --- a/Sources/_TestingInternals/GDI+.cpp +++ /dev/null @@ -1,59 +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 -// - -#include "GDI+.h" - -#include -#include - -using Gdiplus; - -ULONG_PTR swt_gdiplus_startup(int *outError) { - ULONG_PTR result = nullptr; - - GdiplusStartupInput input; - auto status = GdiplusStartup(&result, &input, nullptr); - - if (status != Ok) { - *outError = static_cast(status); - } - return result; -} - -void swt_gdiplus_shutdown(ULONG_PTR *token) { - (void)GdiplusShutdown(token); -} - -void *swt_gdiplus_createImageFromHBITMAP(HBITMAP bitmap, HPALETTE palette) { - return Bitmap::FromHBITMAP(bitmap, palette); -} - -void swt_gdiplus_destroyImage(void *image) { - auto bitmap = reinterpret_cast(image); - delete bitmap; -} - -void *swt_gdiplus_copyBytes(void *image, const CLSID *clsid, size_t *outByteCount, int *outError) { - auto bitmap = reinterpret_cast(image); - - // Create an IStream in memory and save the image to it. - auto stream = SHCreateMemStream(nullptr, 0); - auto status = bitmap->Save(stream, clsid); - - // Read back from the stream into - (void)stream->Seek(0, STREAM_SEEK_SET, nullptr); - - stream->Release(); - - if (status != Ok) { - *outError = static_cast(status); - } - return nullptr; -} diff --git a/Sources/_TestingInternals/include/GDI+.h b/Sources/_TestingInternals/include/GDI+.h deleted file mode 100644 index 27be026c9..000000000 --- a/Sources/_TestingInternals/include/GDI+.h +++ /dev/null @@ -1,31 +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 - -#if defined(_WIN32) - -#include "Defines.h" -#include "Includes.h" - -SWT_ASSUME_NONNULL_BEGIN -SWT_EXTERN ULONG_PTR swt_gdiplus_startup(int *outError); -SWT_EXTERN void swt_gdiplus_shutdown(ULONG_PTR *token); - -SWT_EXTERN void *swt_gdiplus_createImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette); -SWT_EXTERN void swt_gdiplus_destroyImage(void *image); - -SWT_EXTERN void *_Nullable swt_gdiplus_copyBytes(void *image, const CLSID *clsid, size_t *outByteCount, int *outError); -SWT_ASSUME_NONNULL_END - -#endif - -#endif // SWT_DEFINES_H diff --git a/Sources/_TestingInternals/include/Includes.h b/Sources/_TestingInternals/include/Includes.h index 869fcff2a..3f0433cb4 100644 --- a/Sources/_TestingInternals/include/Includes.h +++ b/Sources/_TestingInternals/include/Includes.h @@ -153,7 +153,6 @@ #endif #if defined(_WIN32) -#define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #include From 3b773809d18f8250eaf39f6ed310b5597ffdc29a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 18:20:10 -0400 Subject: [PATCH 03/25] HBITMAP working now --- Package.swift | 3 +- .../AttachableAsGDIPlusImage.swift | 12 +-- .../AttachableImageFormat+CLSID.swift | 57 ++++++++++++++ .../Attachment+AttachableAsGDIPlusImage.swift | 37 +++++++++ .../_Testing_WinSDK/Attachments/GDI+.swift | 13 ++-- .../HBITMAP+AttachableAsGDIPlusImage.swift | 32 +++++--- .../Attachments/_AttachableImageWrapper.swift | 77 +++++++++++++++++++ Sources/_Gdiplus/include/Includes.h | 17 ++++ Sources/_Gdiplus/include/module.modulemap | 2 + .../_TestingInternals/include/TestSupport.h | 6 ++ Tests/TestingTests/AttachmentTests.swift | 35 +++++++++ 11 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift diff --git a/Package.swift b/Package.swift index e2f1a9672..8b8b270ec 100644 --- a/Package.swift +++ b/Package.swift @@ -260,8 +260,7 @@ let package = Package( #endif }(), path: "Sources/Overlays/_Testing_WinSDK", - swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)], - linkerSettings: [.linkedLibrary("Gdiplus.lib", .when(platforms: [.windows]))] + swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)] ), // 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 index 733c6a90a..d1ac483eb 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -19,7 +19,7 @@ public protocol AttachableAsGDIPlusImage { /// - Warning: Do not call this function directly. Instead, call ``withGDIPlusImage(for:_:)``. func _withGDIPlusImage( for attachment: borrowing Attachment>, - _ body: (UnsafeRawPointer) throws -> R + _ body: (OpaquePointer) throws -> R ) throws -> R } @@ -27,12 +27,14 @@ extension AttachableAsGDIPlusImage { /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. - public func withGDIPlusImage( + func withGDIPlusImage( for attachment: borrowing Attachment>, - _ body: (UnsafeRawPointer) throws -> R + _ body: (OpaquePointer) throws -> R ) throws -> R { - try withGDIPlus(for: attachment) { attachment in - try self._withGDIPlusImage(for: attachment, body) + try withUnsafePointer(to: attachment) { attachment in + try withGDIPlus { + try self._withGDIPlusImage(for: attachment.pointee, body) + } } } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift new file mode 100644 index 000000000..82e6bdc8d --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -0,0 +1,57 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024–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 + +public import WinSDK +private import _Gdiplus + +extension AttachableImageFormat { + private static nonisolated(unsafe) let _allImageCodecInfo: UnsafeBufferPointer? = { + try? withGDIPlus { + var encoderCount = UINT(0) + var byteCount = UINT(0) + let rGetSize = Gdiplus.GetImageEncodersSize(&encoderCount, &byteCount) + guard rGetSize == Gdiplus.Ok else { + return nil + } + + let result = UnsafeMutableRawBufferPointer + .allocate(byteCount: Int(byteCount), alignment: MemoryLayout.alignment) + .bindMemory(to: Gdiplus.ImageCodecInfo.self) + let rGetEncoders = Gdiplus.GetImageEncoders(encoderCount, byteCount, result.baseAddress!) + guard rGetEncoders == Gdiplus.Ok else { + result.deallocate() + return nil + } + + return .init(result) + } + }() + + private static func _clsid(forMIMEType mimeType: String) -> CLSID? { + mimeType.withCString(encodedAs: UTF16.self) { mimeType in + _allImageCodecInfo?.first { 0 == wcscmp($0.MimeType, mimeType) }?.Clsid + } + } + + var clsid: CLSID? { + switch kind { + case .png: + Self._clsid(forMIMEType: "image/png") + case .jpeg: + Self._clsid(forMIMEType: "image/jpeg") + default: + fatalError("Unimplemented: custom image formats on Windows") + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..c71785464 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -0,0 +1,37 @@ +// +// 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) public import Testing + +@_spi(Experimental) +@available(_uttypesAPI, *) +extension Attachment { + public init( + _ attachableValue: T, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) + self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) + } + + public static func record( + _ image: consuming T, + named preferredName: String? = nil, + as imageFormat: AttachableImageFormat? = nil, + sourceLocation: SourceLocation = #_sourceLocation + ) where AttachableValue == _AttachableImageWrapper { + let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) + Self.record(attachment, sourceLocation: sourceLocation) + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index f7082bf09..1764e76d8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -9,27 +9,24 @@ // #if os(Windows) -internal import WinSDK +import WinSDK private import _Gdiplus struct GDIPlusError: Error, RawRepresentable { - var rawValue: CInt + var rawValue: Gdiplus.Status } -func withGDIPlus( - for attachmenthment: borrowing Attachment, - _ body: (borrowing Attachment) throws -> R -) throws -> R where A: ~Copyable { +func withGDIPlus(_ body: () throws -> R) throws -> R { var token = ULONG_PTR(0) var input = Gdiplus.GdiplusStartupInput(nil, false, false) let rStartup = swt_winsdk_GdiplusStartup(&token, &input, nil) guard rStartup == Gdiplus.Ok else { - throw GDIPlusError(rawValue: rStartup.rawValue) + throw GDIPlusError(rawValue: rStartup) } defer { swt_winsdk_GdiplusShutdown(token) } - return try body(attachmenthment) + return try body() } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index c5ff52aa9..5008b70a8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -9,16 +9,18 @@ // #if os(Windows) -public import Testing +@_spi(Experimental) public import Testing +// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly public import WinSDK private import _Gdiplus +@_spi(Experimental) public final class _AttachableHBITMAPWrapper { private let _bitmap: HBITMAP private let _palette: HPALETTE? - init(bitmap: consuming HBITMAP, palette: consuming HPALETTE? = nil) { + fileprivate init(bitmap: consuming HBITMAP, palette: consuming HPALETTE? = nil) { _bitmap = bitmap _palette = palette } @@ -31,37 +33,49 @@ public final class _AttachableHBITMAPWrapper { } } +@_spi(Experimental) extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { public func _withGDIPlusImage( - for attachment: Attachable>, - _ body: (UnsafeMutableRawPointer) throws -> R + for attachment: borrowing Attachment>, + _ body: (OpaquePointer) throws -> R ) throws -> R { - let image = swt_winsdk_GdiplusBitmapCreate(_bitmap, _palette) + guard let image = swt_winsdk_GdiplusBitmapCreate(_bitmap, _palette) else { + print("swt_winsdk_GdiplusBitmapCreate: \(Gdiplus.GenericError)") + throw GDIPlusError(rawValue: Gdiplus.GenericError) + } + defer { + swt_winsdk_GdiplusImageDelete(image) + } return try withExtendedLifetime(self) { try body(image) } } } +@_spi(Experimental) extension Attachment where AttachableValue == _AttachableImageWrapper<_AttachableHBITMAPWrapper> { public init( - _ bitmap: consuming WinSDK.HBITMAP, - with palette: consuming WinSDK.HPALETTE? = nil, + _ bitmap: consuming UnsafeMutableRawPointer, + with palette: consuming UnsafeMutableRawPointer? = nil, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) { + let bitmap = bitmap.assumingMemoryBound(to: HBITMAP__.self) + let palette = palette.map { $0.assumingMemoryBound(to: HPALETTE__.self) } let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) self.init(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) } public static func record( - _ bitmap: consuming HBITMAP, - with palette: consuming HPALETTE? = nil, + _ bitmap: consuming UnsafeMutableRawPointer, + with palette: consuming UnsafeMutableRawPointer? = nil, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) { + let bitmap = bitmap.assumingMemoryBound(to: HBITMAP__.self) + let palette = palette.map { $0.assumingMemoryBound(to: HPALETTE__.self) } let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) Self.record(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift new file mode 100644 index 000000000..79015e603 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -0,0 +1,77 @@ +// +// 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) public import Testing + +import WinSDK +private import _Gdiplus + +@_spi(Experimental) +public struct _AttachableImageWrapper: Sendable where Image: AttachableAsGDIPlusImage { + /// The underlying image. + /// + /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` + /// instances can be created from closures that are run at rendering time. + /// The AppKit cross-import overlay is responsible for ensuring that any + /// instances of this type it creates hold "safe" `NSImage` instances. + nonisolated(unsafe) var image: Image + + /// The image format to use when encoding the represented image. + var imageFormat: AttachableImageFormat? +} + +// MARK: - + +@available(_uttypesAPI, *) +extension _AttachableImageWrapper: AttachableWrapper { + public var wrappedValue: Image { + image + } + + public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { + var stream: UnsafeMutablePointer? + let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) + guard S_OK == rCreateStream else { + fatalError("FIXME: throw as HRESULT") + } + defer { + swt_winsdk_IStreamRelease(stream) + } + + let imageFormat = self.imageFormat ?? .png + guard var clsid = imageFormat.clsid else { + fatalError("FIXME: throw format-not-supported error") + } + + try image.withGDIPlusImage(for: attachment) { image in + let rSave = swt_winsdk_GdiplusImageSave(image, stream, &clsid, nil) + guard rSave == Gdiplus.Ok else { + print("failed to save: \(rSave)") + throw GDIPlusError(rawValue: rSave) + } + } + + var global: HGLOBAL? + let rGetGlobal = GetHGlobalFromStream(stream, &global) + guard S_OK == rGetGlobal else { + fatalError("FIXME: throw as HRESULT") + } + guard let baseAddress = GlobalLock(global) else { + fatalError("FIXME: throw bad-memory error") + } + defer { + GlobalUnlock(global) + } + let byteCount = GlobalSize(global) + return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) + } +} +#endif diff --git a/Sources/_Gdiplus/include/Includes.h b/Sources/_Gdiplus/include/Includes.h index be40c5f8c..6018eb56e 100644 --- a/Sources/_Gdiplus/include/Includes.h +++ b/Sources/_Gdiplus/include/Includes.h @@ -29,4 +29,21 @@ static inline void swt_winsdk_GdiplusShutdown(ULONG_PTR token) { static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HBITMAP bitmap, HPALETTE palette) { return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); } + +static inline void swt_winsdk_GdiplusImageDelete(Gdiplus::Image *image) { + delete image; +} + +static inline Gdiplus::Status swt_winsdk_GdiplusImageSave( + Gdiplus::Image *image, + IStream *stream, + const CLSID *format, + const Gdiplus::EncoderParameters *encoderParams +) { + return image->Save(stream, format, encoderParams); +} + +static inline void swt_winsdk_IStreamRelease(IStream *stream) { + stream->Release(); +} #endif diff --git a/Sources/_Gdiplus/include/module.modulemap b/Sources/_Gdiplus/include/module.modulemap index 73022d14a..2d63bd95d 100644 --- a/Sources/_Gdiplus/include/module.modulemap +++ b/Sources/_Gdiplus/include/module.modulemap @@ -11,4 +11,6 @@ module _Gdiplus { umbrella "." export * + + link "Gdiplus.lib" } diff --git a/Sources/_TestingInternals/include/TestSupport.h b/Sources/_TestingInternals/include/TestSupport.h index 37d42692e..2d6229ed5 100644 --- a/Sources/_TestingInternals/include/TestSupport.h +++ b/Sources/_TestingInternals/include/TestSupport.h @@ -37,6 +37,12 @@ static inline bool swt_pointersNotEqual4(const char *a, const char *b, const cha return a != b && b != c && c != d; } +#if defined(_WIN32) +static inline LPCSTR swt_IDI_SHIELD(void) { + return IDI_SHIELD; +} +#endif + SWT_ASSUME_NONNULL_END #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 6cc30e608..6d38d761c 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -33,6 +33,10 @@ import UIKit #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif +#if canImport(WinSDK) && canImport(_Testing_WinSDK) +import WinSDK +@_spi(Experimental) import _Testing_WinSDK +#endif @Suite("Attachment Tests") struct AttachmentTests { @@ -694,6 +698,37 @@ extension AttachmentTests { } } #endif +#endif + +#if canImport(WinSDK) && canImport(_Testing_WinSDK) + @MainActor @Test func attachHBITMAP() throws { + let (width, height) = (GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) + + let icon = try #require(LoadIconA(nil, swt_IDI_SHIELD())) + defer { + DeleteObject(icon) + } + + let screenDC = try #require(GetDC(nil)) + defer { + ReleaseDC(nil, screenDC) + } + + let dc = try #require(CreateCompatibleDC(nil)) + defer { + DeleteDC(dc) + } + + let bitmap = try #require(CreateCompatibleBitmap(screenDC, width, height)) + SelectObject(dc, bitmap) + DrawIcon(dc, 0, 0, icon) + + let attachment = Attachment<_AttachableImageWrapper<_AttachableHBITMAPWrapper>>(bitmap, with: nil, named: "diamond.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + Attachment.record(attachment) + } #endif } } From 7737882cae89e8fbb9059cc6b244788d09467a3b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 19:13:57 -0400 Subject: [PATCH 04/25] Documentation comments etc. --- .../AttachableAsGDIPlusImage.swift | 3 + .../AttachableImageFormat+CLSID.swift | 77 ++++++++++++++++--- .../_Testing_WinSDK/Attachments/GDI+.swift | 11 ++- .../HBITMAP+AttachableAsGDIPlusImage.swift | 56 +------------- .../Attachments/_AttachableImageWrapper.swift | 21 +++-- 5 files changed, 97 insertions(+), 71 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index d1ac483eb..73854db61 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -31,6 +31,9 @@ extension AttachableAsGDIPlusImage { for attachment: borrowing Attachment>, _ body: (OpaquePointer) throws -> R ) throws -> R { + // Stuff the attachment into a pointer so we can reference it from within + // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't + // reason about the lifetime of a borrowed value passed into a closure.) try withUnsafePointer(to: attachment) { attachment in try withGDIPlus { try self._withGDIPlusImage(for: attachment.pointee, body) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 82e6bdc8d..ef439411a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -15,8 +15,13 @@ public import WinSDK private import _Gdiplus 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`. private static nonisolated(unsafe) let _allImageCodecInfo: UnsafeBufferPointer? = { try? withGDIPlus { + // Find out the size of the buffer needed. var encoderCount = UINT(0) var byteCount = UINT(0) let rGetSize = Gdiplus.GetImageEncodersSize(&encoderCount, &byteCount) @@ -24,34 +29,86 @@ extension AttachableImageFormat { return nil } - let result = UnsafeMutableRawBufferPointer - .allocate(byteCount: Int(byteCount), alignment: MemoryLayout.alignment) + // 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 encoderBuffer = result + .prefix(MemoryLayout.stride * Int(encoderCount)) .bindMemory(to: Gdiplus.ImageCodecInfo.self) - let rGetEncoders = Gdiplus.GetImageEncoders(encoderCount, byteCount, result.baseAddress!) + + // Read the encoders list. + let rGetEncoders = Gdiplus.GetImageEncoders(encoderCount, byteCount, encoderBuffer.baseAddress!) guard rGetEncoders == Gdiplus.Ok else { result.deallocate() return nil } - - return .init(result) + return .init(encoderBuffer) } }() + /// Get a `CLSID` value corresponding to the image format with the given MIME + /// type. + /// + /// - Parameters: + /// - mimeType: The MIME type of the image format of interest. + /// + /// - Returns: A `CLSID` value suitable for use with GDI+, or `nil` if none + /// was found corresponding to `mimeType`. private static func _clsid(forMIMEType mimeType: String) -> CLSID? { mimeType.withCString(encodedAs: UTF16.self) { mimeType in _allImageCodecInfo?.first { 0 == wcscmp($0.MimeType, mimeType) }?.Clsid } } - var clsid: CLSID? { + /// 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 = _clsid(forMIMEType: "image/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 = _clsid(forMIMEType: "image/jpeg") + + /// The `CLSID` value corresponding to this image format. + public var clsid: CLSID? { switch kind { case .png: - Self._clsid(forMIMEType: "image/png") + Self._pngCLSID case .jpeg: - Self._clsid(forMIMEType: "image/jpeg") - default: - fatalError("Unimplemented: custom image formats on Windows") + Self._jpegCLSID + case let .systemValue(clsid): + clsid as? CLSID } } + + /// Initialize an instance of this type with the given `CLSID` value` and + /// encoding quality. + /// + /// - Parameters: + /// - clsid: The `CLSID` value corresponding to the image format 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, + /// 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. + public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { + self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) + } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index 1764e76d8..fc2d151c8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -9,11 +9,16 @@ // #if os(Windows) +@_spi(Experimental) import Testing + import WinSDK private import _Gdiplus -struct GDIPlusError: Error, RawRepresentable { - var rawValue: Gdiplus.Status +enum GDIPlusError: Error { + case status(Gdiplus.Status) + case hresult(HRESULT) + case win32Error(DWORD) + case clsidNotFoundForImageFormat(AttachableImageFormat) } func withGDIPlus(_ body: () throws -> R) throws -> R { @@ -21,7 +26,7 @@ func withGDIPlus(_ body: () throws -> R) throws -> R { var input = Gdiplus.GdiplusStartupInput(nil, false, false) let rStartup = swt_winsdk_GdiplusStartup(&token, &input, nil) guard rStartup == Gdiplus.Ok else { - throw GDIPlusError(rawValue: rStartup) + throw GDIPlusError.status(rStartup) } defer { swt_winsdk_GdiplusShutdown(token) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 5008b70a8..f9e02206c 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -16,32 +16,13 @@ public import WinSDK private import _Gdiplus @_spi(Experimental) -public final class _AttachableHBITMAPWrapper { - private let _bitmap: HBITMAP - private let _palette: HPALETTE? - - fileprivate init(bitmap: consuming HBITMAP, palette: consuming HPALETTE? = nil) { - _bitmap = bitmap - _palette = palette - } - - deinit { - DeleteObject(_bitmap) - if let _palette { - DeleteObject(_palette) - } - } -} - -@_spi(Experimental) -extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { +extension WinSDK.HBITMAP: AttachableAsGDIPlusImage { public func _withGDIPlusImage( - for attachment: borrowing Attachment>, + for attachment: borrowing Attachment>, _ body: (OpaquePointer) throws -> R ) throws -> R { - guard let image = swt_winsdk_GdiplusBitmapCreate(_bitmap, _palette) else { - print("swt_winsdk_GdiplusBitmapCreate: \(Gdiplus.GenericError)") - throw GDIPlusError(rawValue: Gdiplus.GenericError) + guard let image = swt_winsdk_GdiplusBitmapCreate(self, nil) else { + throw GDIPlusError.status(Gdiplus.GenericError) } defer { swt_winsdk_GdiplusImageDelete(image) @@ -51,33 +32,4 @@ extension _AttachableHBITMAPWrapper: AttachableAsGDIPlusImage { } } } - -@_spi(Experimental) -extension Attachment where AttachableValue == _AttachableImageWrapper<_AttachableHBITMAPWrapper> { - public init( - _ bitmap: consuming UnsafeMutableRawPointer, - with palette: consuming UnsafeMutableRawPointer? = nil, - named preferredName: String? = nil, - as imageFormat: AttachableImageFormat? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - let bitmap = bitmap.assumingMemoryBound(to: HBITMAP__.self) - let palette = palette.map { $0.assumingMemoryBound(to: HPALETTE__.self) } - let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) - self.init(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) - } - - public static func record( - _ bitmap: consuming UnsafeMutableRawPointer, - with palette: consuming UnsafeMutableRawPointer? = nil, - named preferredName: String? = nil, - as imageFormat: AttachableImageFormat? = nil, - sourceLocation: SourceLocation = #_sourceLocation - ) { - let bitmap = bitmap.assumingMemoryBound(to: HBITMAP__.self) - let palette = palette.map { $0.assumingMemoryBound(to: HPALETTE__.self) } - let bitmapWrapper = _AttachableHBITMAPWrapper(bitmap: bitmap, palette: palette) - Self.record(bitmapWrapper, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) - } -} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 79015e603..4c6ecfc0f 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -37,35 +37,44 @@ extension _AttachableImageWrapper: AttachableWrapper { } public func withUnsafeBytes(for attachment: borrowing Attachment, _ 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. var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) guard S_OK == rCreateStream else { - fatalError("FIXME: throw as HRESULT") + throw GDIPlusError.hresult(rCreateStream) } defer { swt_winsdk_IStreamRelease(stream) } + // Get the CLSID of the image encoder corresponding to the specified image + // format. + // TODO: infer an image format from the filename like we do on Darwin. let imageFormat = self.imageFormat ?? .png guard var clsid = imageFormat.clsid else { - fatalError("FIXME: throw format-not-supported error") + throw GDIPlusError.clsidNotFoundForImageFormat(imageFormat) } + // Save the image into the stream. try image.withGDIPlusImage(for: attachment) { image in let rSave = swt_winsdk_GdiplusImageSave(image, stream, &clsid, nil) guard rSave == Gdiplus.Ok else { - print("failed to save: \(rSave)") - throw GDIPlusError(rawValue: rSave) + throw GDIPlusError.status(rSave) } } + // Extract the serialized image and pass it back to the caller. We hold the + // HGLOBAL locked while calling `body`, but nothing else should have a + // reference to it. var global: HGLOBAL? let rGetGlobal = GetHGlobalFromStream(stream, &global) guard S_OK == rGetGlobal else { - fatalError("FIXME: throw as HRESULT") + throw GDIPlusError.hresult(rGetGlobal) } guard let baseAddress = GlobalLock(global) else { - fatalError("FIXME: throw bad-memory error") + throw GDIPlusError.win32Error(GetLastError()) } defer { GlobalUnlock(global) From fa9092332e08276aa663b1195c3587b549dd1d58 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 19:42:27 -0400 Subject: [PATCH 05/25] Opt-out of calling GdiplusStartup() if needed --- .../Overlays/_Testing_WinSDK/Attachments/GDI+.swift | 6 ++++++ Sources/Testing/Support/Environment.swift | 10 +++++----- Tests/TestingTests/AttachmentTests.swift | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index fc2d151c8..e63de5118 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -22,6 +22,12 @@ enum GDIPlusError: Error { } 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_winsdk_GdiplusStartup(&token, &input, nil) diff --git a/Sources/Testing/Support/Environment.swift b/Sources/Testing/Support/Environment.swift index 2ab3710a4..4cddde9e0 100644 --- a/Sources/Testing/Support/Environment.swift +++ b/Sources/Testing/Support/Environment.swift @@ -15,7 +15,7 @@ private import _TestingInternals /// This type can be used to access the current process' environment variables. /// /// This type is not part of the public interface of the testing library. -enum Environment { +package enum Environment { #if SWT_NO_ENVIRONMENT_VARIABLES /// Storage for the simulated environment. /// @@ -92,7 +92,7 @@ enum Environment { /// Get all environment variables in the current process. /// /// - Returns: A copy of the current process' environment dictionary. - static func get() -> [String: String] { + package static func get() -> [String: String] { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue #elseif SWT_TARGET_OS_APPLE @@ -140,7 +140,7 @@ enum Environment { /// /// - Returns: The value of the specified environment variable, or `nil` if it /// is not set for the current process. - static func variable(named name: String) -> String? { + package static func variable(named name: String) -> String? { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.rawValue[name] #elseif SWT_TARGET_OS_APPLE && !SWT_NO_DYNAMIC_LINKING @@ -221,7 +221,7 @@ enum Environment { /// - String values beginning with the letters `"t"`, `"T"`, `"y"`, or `"Y"` /// are interpreted as `true`; and /// - All other non-`nil` string values are interpreted as `false`. - static func flag(named name: String) -> Bool? { + package static func flag(named name: String) -> Bool? { variable(named: name).map { if let signedValue = Int64($0) { return signedValue != 0 @@ -248,7 +248,7 @@ extension Environment { /// /// - Returns: Whether or not the environment variable was successfully set. @discardableResult - static func setVariable(_ value: String?, named name: String) -> Bool { + package static func setVariable(_ value: String?, named name: String) -> Bool { #if SWT_NO_ENVIRONMENT_VARIABLES simulatedEnvironment.withLock { environment in environment[name] = value diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 6d38d761c..56c682cd8 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -723,7 +723,7 @@ extension AttachmentTests { SelectObject(dc, bitmap) DrawIcon(dc, 0, 0, icon) - let attachment = Attachment<_AttachableImageWrapper<_AttachableHBITMAPWrapper>>(bitmap, with: nil, named: "diamond.png") + let attachment = Attachment(bitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) } From 00475be0a7879084c2d7183f1ca186bd43bef74c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 20:33:50 -0400 Subject: [PATCH 06/25] Add HICON support, describe errors --- .../AttachableAsGDIPlusImage.swift | 13 +++++++ .../_Testing_WinSDK/Attachments/GDI+.swift | 23 ++++++++++-- .../HBITMAP+AttachableAsGDIPlusImage.swift | 35 ------------------- .../Attachments/_AttachableImageWrapper.swift | 8 ++--- Sources/Testing/Support/CError.swift | 10 ++++-- Sources/_Gdiplus/include/Includes.h | 33 ----------------- Tests/TestingTests/AttachmentTests.swift | 11 +++++- 7 files changed, 54 insertions(+), 79 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 73854db61..44a4171b2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -41,4 +41,17 @@ extension AttachableAsGDIPlusImage { } } } + +public protocol _AttachableByAddressAsGDIPlusImage { + /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) + /// by design. The caller is responsible for guarding against concurrent + /// access to the resulting GDI+ image object. + /// + /// - Warning: Do not call this function directly. Instead, call ``withGDIPlusImage(for:_:)``. + static func _withGDIPlusImage( + _ address: UnsafeMutablePointer, + for attachment: borrowing Attachment>>, + _ body: (OpaquePointer) throws -> R + ) throws -> R +} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index e63de5118..ac08a111d 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -16,11 +16,28 @@ private import _Gdiplus enum GDIPlusError: Error { case status(Gdiplus.Status) - case hresult(HRESULT) - case win32Error(DWORD) - case clsidNotFoundForImageFormat(AttachableImageFormat) + case streamCreationFailed(HRESULT) + case globalFromStreamFailed(HRESULT) + case clsidNotFound } +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))." + case .clsidNotFound: + "Could not find an appropriate CSLID value for the specified image format." + } + } +} + +// MARK: - + 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. 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 f9e02206c..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ /dev/null @@ -1,35 +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) public import Testing - -// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly -public import WinSDK -private import _Gdiplus - -@_spi(Experimental) -extension WinSDK.HBITMAP: AttachableAsGDIPlusImage { - public func _withGDIPlusImage( - for attachment: borrowing Attachment>, - _ body: (OpaquePointer) throws -> R - ) throws -> R { - guard let image = swt_winsdk_GdiplusBitmapCreate(self, nil) else { - throw GDIPlusError.status(Gdiplus.GenericError) - } - defer { - swt_winsdk_GdiplusImageDelete(image) - } - return try withExtendedLifetime(self) { - try body(image) - } - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 4c6ecfc0f..ce285a64f 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -43,7 +43,7 @@ extension _AttachableImageWrapper: AttachableWrapper { var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) guard S_OK == rCreateStream else { - throw GDIPlusError.hresult(rCreateStream) + throw GDIPlusError.streamCreationFailed(rCreateStream) } defer { swt_winsdk_IStreamRelease(stream) @@ -54,7 +54,7 @@ extension _AttachableImageWrapper: AttachableWrapper { // TODO: infer an image format from the filename like we do on Darwin. let imageFormat = self.imageFormat ?? .png guard var clsid = imageFormat.clsid else { - throw GDIPlusError.clsidNotFoundForImageFormat(imageFormat) + throw GDIPlusError.clsidNotFound } // Save the image into the stream. @@ -71,10 +71,10 @@ extension _AttachableImageWrapper: AttachableWrapper { var global: HGLOBAL? let rGetGlobal = GetHGlobalFromStream(stream, &global) guard S_OK == rGetGlobal else { - throw GDIPlusError.hresult(rGetGlobal) + throw GDIPlusError.globalFromStreamFailed(rGetGlobal) } guard let baseAddress = GlobalLock(global) else { - throw GDIPlusError.win32Error(GetLastError()) + throw Win32Error(rawValue: GetLastError()) } defer { GlobalUnlock(global) diff --git a/Sources/Testing/Support/CError.swift b/Sources/Testing/Support/CError.swift index a8462fda4..572775527 100644 --- a/Sources/Testing/Support/CError.swift +++ b/Sources/Testing/Support/CError.swift @@ -27,8 +27,12 @@ struct CError: Error, RawRepresentable { /// [here](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes). /// /// This type is not part of the public interface of the testing library. -struct Win32Error: Error, RawRepresentable { - var rawValue: DWORD +package struct Win32Error: Error, RawRepresentable { + package var rawValue: CUnsignedInt + + package init(rawValue: CUnsignedInt) { + self.rawValue = rawValue + } } #endif @@ -66,7 +70,7 @@ extension CError: CustomStringConvertible { #if os(Windows) extension Win32Error: CustomStringConvertible { - var description: String { + package var description: String { let (address, count) = withUnsafeTemporaryAllocation(of: LPWSTR?.self, capacity: 1) { buffer in // FormatMessageW() takes a wide-character buffer into which it writes the // error message... _unless_ you pass `FORMAT_MESSAGE_ALLOCATE_BUFFER` in diff --git a/Sources/_Gdiplus/include/Includes.h b/Sources/_Gdiplus/include/Includes.h index 6018eb56e..ae6f5d68b 100644 --- a/Sources/_Gdiplus/include/Includes.h +++ b/Sources/_Gdiplus/include/Includes.h @@ -13,37 +13,4 @@ #include #include - -static inline Gdiplus::Status swt_winsdk_GdiplusStartup( - ULONG_PTR *token, - const Gdiplus::GdiplusStartupInput *input, - Gdiplus::GdiplusStartupOutput *output -) { - return Gdiplus::GdiplusStartup(token, input, output); -} - -static inline void swt_winsdk_GdiplusShutdown(ULONG_PTR token) { - Gdiplus::GdiplusShutdown(token); -} - -static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HBITMAP bitmap, HPALETTE palette) { - return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); -} - -static inline void swt_winsdk_GdiplusImageDelete(Gdiplus::Image *image) { - delete image; -} - -static inline Gdiplus::Status swt_winsdk_GdiplusImageSave( - Gdiplus::Image *image, - IStream *stream, - const CLSID *format, - const Gdiplus::EncoderParameters *encoderParams -) { - return image->Save(stream, format, encoderParams); -} - -static inline void swt_winsdk_IStreamRelease(IStream *stream) { - stream->Release(); -} #endif diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 5a8f739a8..e5f7b3d47 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -701,6 +701,15 @@ extension AttachmentTests { #endif #if canImport(WinSDK) && canImport(_Testing_WinSDK) + @MainActor @Test func attachHICON() throws { + let icon = try #require(LoadIconA(nil, swt_IDI_SHIELD())) + let attachment = Attachment(icon, named: "square.png") + try attachment.withUnsafeBytes { buffer in + #expect(buffer.count > 32) + } + Attachment.record(attachment) + } + @MainActor @Test func attachHBITMAP() throws { let (width, height) = (GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) @@ -722,7 +731,7 @@ extension AttachmentTests { let bitmap = try #require(CreateCompatibleBitmap(screenDC, width, height)) SelectObject(dc, bitmap) DrawIcon(dc, 0, 0, icon) - + let attachment = Attachment(bitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) From c5dc10b8f036b3816ae2c8a3295fcad426d99c43 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 20:50:59 -0400 Subject: [PATCH 07/25] Refactor around pointers --- .../AttachableAsGDIPlusImage.swift | 27 ++++++------------- .../Attachment+AttachableAsGDIPlusImage.swift | 14 +++++----- .../Attachments/_AttachableImageWrapper.swift | 17 +++++------- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 44a4171b2..55a4d16b8 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -16,14 +16,16 @@ public protocol AttachableAsGDIPlusImage { /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. /// - /// - Warning: Do not call this function directly. Instead, call ``withGDIPlusImage(for:_:)``. - func _withGDIPlusImage( - for attachment: borrowing Attachment>, + /// - Warning: Do not call this function directly. Instead, call + /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. + static func _withGDIPlusImage( + at address: P, + for attachment: borrowing Attachment>, _ body: (OpaquePointer) throws -> R - ) throws -> R + ) throws -> R where P: _Pointer, P.Pointee == Self } -extension AttachableAsGDIPlusImage { +extension _Pointer where Pointee: AttachableAsGDIPlusImage { /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. @@ -36,22 +38,9 @@ extension AttachableAsGDIPlusImage { // reason about the lifetime of a borrowed value passed into a closure.) try withUnsafePointer(to: attachment) { attachment in try withGDIPlus { - try self._withGDIPlusImage(for: attachment.pointee, body) + try Pointee._withGDIPlusImage(at: self, for: attachment.pointee, body) } } } } - -public protocol _AttachableByAddressAsGDIPlusImage { - /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) - /// by design. The caller is responsible for guarding against concurrent - /// access to the resulting GDI+ image object. - /// - /// - Warning: Do not call this function directly. Instead, call ``withGDIPlusImage(for:_:)``. - static func _withGDIPlusImage( - _ address: UnsafeMutablePointer, - for attachment: borrowing Attachment>>, - _ body: (OpaquePointer) throws -> R - ) throws -> R -} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift index c71785464..a718f3d9a 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -14,22 +14,22 @@ @_spi(Experimental) @available(_uttypesAPI, *) extension Attachment { - public init( - _ attachableValue: T, + public init

( + _ attachableValue: P, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat) + ) where AttachableValue == _AttachableImageWrapper

{ + let imageWrapper = _AttachableImageWrapper(pointer: attachableValue, imageFormat: imageFormat) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } - public static func record( - _ image: consuming T, + public static func record

( + _ image: P, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper { + ) where AttachableValue == _AttachableImageWrapper

{ let attachment = Self(image, named: preferredName, as: imageFormat, 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 ce285a64f..1c79f3f75 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -15,14 +15,9 @@ import WinSDK private import _Gdiplus @_spi(Experimental) -public struct _AttachableImageWrapper: Sendable where Image: AttachableAsGDIPlusImage { - /// The underlying image. - /// - /// `CGImage` and `UIImage` are sendable, but `NSImage` is not. `NSImage` - /// instances can be created from closures that are run at rendering time. - /// The AppKit cross-import overlay is responsible for ensuring that any - /// instances of this type it creates hold "safe" `NSImage` instances. - nonisolated(unsafe) var image: Image +public struct _AttachableImageWrapper: Sendable where Pointer: _Pointer, Pointer.Pointee: AttachableAsGDIPlusImage { + /// A pointer to the underlying image. + nonisolated(unsafe) var pointer: Pointer /// The image format to use when encoding the represented image. var imageFormat: AttachableImageFormat? @@ -32,8 +27,8 @@ public struct _AttachableImageWrapper: Sendable where Image: AttachableAs @available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Image { - image + public var wrappedValue: Pointer { + pointer } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { @@ -58,7 +53,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Save the image into the stream. - try image.withGDIPlusImage(for: attachment) { image in + try pointer.withGDIPlusImage(for: attachment) { image in let rSave = swt_winsdk_GdiplusImageSave(image, stream, &clsid, nil) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) From 3a420d7c8944015746844992e442ff6be7fdc3de Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 29 Jul 2025 20:50:59 -0400 Subject: [PATCH 08/25] Missing files --- .../HBITMAP+AttachableAsGDIPlusImage.swift | 37 +++++++++++++ .../HICON+AttachableAsGDIPlusImage.swift | 37 +++++++++++++ Sources/_Gdiplus/include/Stubs.h | 52 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift create mode 100644 Sources/_Gdiplus/include/Stubs.h diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..64fcb64d1 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -0,0 +1,37 @@ +// +// 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) public import Testing + +// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly +public import WinSDK +private import _Gdiplus + +@_spi(Experimental) +extension WinSDK.HBITMAP__: AttachableAsGDIPlusImage { + public static func _withGDIPlusImage( + at address: P, + for attachment: borrowing Attachment>, + _ body: (OpaquePointer) throws -> R + ) throws -> R where P: _Pointer, P.Pointee == Self { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address)) + guard let image = swt_winsdk_GdiplusBitmapCreate(address, nil) else { + throw GDIPlusError.status(Gdiplus.GenericError) + } + defer { + swt_winsdk_GdiplusImageDelete(image) + } + return try withExtendedLifetime(self) { + try body(image) + } + } +} +#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..47faa701f --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -0,0 +1,37 @@ +// +// 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) public import Testing + +// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly +public import WinSDK +private import _Gdiplus + +@_spi(Experimental) +extension WinSDK.HICON__: AttachableAsGDIPlusImage { + public static func _withGDIPlusImage( + at address: P, + for attachment: borrowing Attachment>, + _ body: (OpaquePointer) throws -> R + ) throws -> R where P: _Pointer, P.Pointee == Self { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address)) + guard let image = swt_winsdk_GdiplusBitmapCreate(address) else { + throw GDIPlusError.status(Gdiplus.GenericError) + } + defer { + swt_winsdk_GdiplusImageDelete(image) + } + return try withExtendedLifetime(self) { + try body(image) + } + } +} +#endif diff --git a/Sources/_Gdiplus/include/Stubs.h b/Sources/_Gdiplus/include/Stubs.h new file mode 100644 index 000000000..12b1b2bdf --- /dev/null +++ b/Sources/_Gdiplus/include/Stubs.h @@ -0,0 +1,52 @@ +// +// 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_STUBS_H) +#define SWT_GDIPLUS_STUBS_H + +#include "Includes.h" + +static inline Gdiplus::Status swt_winsdk_GdiplusStartup( + ULONG_PTR *token, + const Gdiplus::GdiplusStartupInput *input, + Gdiplus::GdiplusStartupOutput *output +) { + return Gdiplus::GdiplusStartup(token, input, output); +} + +static inline void swt_winsdk_GdiplusShutdown(ULONG_PTR token) { + Gdiplus::GdiplusShutdown(token); +} + +static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HBITMAP bitmap, HPALETTE palette) { + return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); +} + +static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HICON icon) { + return Gdiplus::Bitmap::FromHICON(icon); +} + +static inline void swt_winsdk_GdiplusImageDelete(Gdiplus::Image *image) { + delete image; +} + +static inline Gdiplus::Status swt_winsdk_GdiplusImageSave( + Gdiplus::Image *image, + IStream *stream, + const CLSID *format, + const Gdiplus::EncoderParameters *encoderParams +) { + return image->Save(stream, format, encoderParams); +} + +static inline void swt_winsdk_IStreamRelease(IStream *stream) { + stream->Release(); +} +#endif From 9d860e646e3d10f816ac1da3e3867b19aee91c1e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 12:36:25 -0400 Subject: [PATCH 09/25] Lots of documentation, rearranging of modules, and general cleanup --- Package.swift | 17 +---- .../AttachableAsGDIPlusImage.swift | 68 +++++++++++++++--- .../AttachableImageFormat+CLSID.swift | 2 +- .../Attachment+AttachableAsGDIPlusImage.swift | 72 +++++++++++++++++-- .../_Testing_WinSDK/Attachments/GDI+.swift | 8 +-- .../HBITMAP+AttachableAsGDIPlusImage.swift | 20 +++--- .../HICON+AttachableAsGDIPlusImage.swift | 20 +++--- .../Attachments/_AttachableImageWrapper.swift | 38 ++++++++-- Sources/_Gdiplus/include/Includes.h | 16 ----- Sources/_Gdiplus/include/Stubs.h | 52 -------------- Sources/_Gdiplus/include/module.modulemap | 16 ----- Sources/_TestingInternals/GDI+/include/GDI+.h | 58 +++++++++++++++ Sources/_TestingInternals/include/Stubs.h | 18 +++++ .../include/module.modulemap | 9 +++ 14 files changed, 272 insertions(+), 142 deletions(-) delete mode 100644 Sources/_Gdiplus/include/Includes.h delete mode 100644 Sources/_Gdiplus/include/Stubs.h delete mode 100644 Sources/_Gdiplus/include/module.modulemap create mode 100644 Sources/_TestingInternals/GDI+/include/GDI+.h diff --git a/Package.swift b/Package.swift index 8b8b270ec..4b541490f 100644 --- a/Package.swift +++ b/Package.swift @@ -252,13 +252,7 @@ let package = Package( name: "_Testing_WinSDK", dependencies: [ "Testing", - ] + { -#if os(Windows) - ["_Gdiplus"] -#else - [] -#endif - }(), + ], path: "Sources/Overlays/_Testing_WinSDK", swiftSettings: .packageSettings + .enableLibraryEvolution() + [.interoperabilityMode(.Cxx)] ), @@ -291,15 +285,6 @@ package.targets.append(contentsOf: [ ]) #endif -#if os(Windows) -package.targets.append(contentsOf: [ - .target( - name: "_Gdiplus", - cxxSettings: .packageSettings - ), -]) -#endif - extension BuildSettingCondition { /// Creates a build setting condition that evaluates to `true` for Embedded /// Swift. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 55a4d16b8..1d6036a37 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -9,28 +9,78 @@ // #if os(Windows) -private import _Gdiplus +@_spi(Experimental) import Testing +private import _TestingInternals.GDIPlus + +internal import WinSDK public protocol AttachableAsGDIPlusImage { - /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) - /// by design. The caller is responsible for guarding against concurrent - /// access to the resulting GDI+ image object. + /// Call a function and pass a GDI+ image representing this instance to it. + /// + /// - Parameters: + /// - address: The address of the instance of this type. + /// - attachment: The attachment that is requesting an image (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A copy of this instance converted to a GDI+ + /// image is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library automatically calls `GdiplusStartup()` and + /// `GdiplusShutdown()` before and after calling this function. This function + /// can therefore assume that GDI+ is correclty configured on the current + /// thread when it is called. + /// + /// - Warning: GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) + /// by design. The caller is responsible for guarding against concurrent + /// access to the resulting GDI+ image object. /// /// - Warning: Do not call this function directly. Instead, call /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. static func _withGDIPlusImage( at address: P, - for attachment: borrowing Attachment>, + for attachment: borrowing Attachment & ~Copyable>, _ body: (OpaquePointer) throws -> R ) throws -> R where P: _Pointer, P.Pointee == Self + + /// Clean up any resources at the given address. + /// + /// - Parameters: + /// - address: The address of the instance of this type. + /// + /// The implementation of this function cleans up any resources (such as + /// handles or COM objects) at `address`. This function is invoked + /// automatically by `_AttachableImageWrapper` when it is deinitialized. + /// + /// - Warning: Do not call this function directly. + static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self } extension _Pointer where Pointee: AttachableAsGDIPlusImage { - /// GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) - /// by design. The caller is responsible for guarding against concurrent - /// access to the resulting GDI+ image object. + /// Call a function and pass a GDI+ image representing this instance to it. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting an image (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A copy of this instance converted to a GDI+ + /// image is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// This function is a convenience wrapper around `_withGDIPlusImage()` that + /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. + /// + /// - Warning: GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) + /// by design. The caller is responsible for guarding against concurrent + /// access to the resulting GDI+ image object. func withGDIPlusImage( - for attachment: borrowing Attachment>, + for attachment: borrowing Attachment & ~Copyable>, _ body: (OpaquePointer) throws -> R ) throws -> R { // Stuff the attachment into a pointer so we can reference it from within diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index ef439411a..198fe1eed 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -10,9 +10,9 @@ #if os(Windows) @_spi(Experimental) import Testing +private import _TestingInternals.GDIPlus public import WinSDK -private import _Gdiplus extension AttachableImageFormat { /// The set of `ImageCodecInfo` instances known to GDI+. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift index a718f3d9a..0ea55e389 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -13,24 +13,86 @@ @_spi(Experimental) @available(_uttypesAPI, *) -extension Attachment { +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. + /// - 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`. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// The following system-provided image types conform to the + /// ``AttachableAsGDIPlusImage`` 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) + /// + /// 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 + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// 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. public init

( - _ attachableValue: P, + _ attachableValue: consuming P, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper

{ - let imageWrapper = _AttachableImageWrapper(pointer: attachableValue, imageFormat: imageFormat) + let imageWrapper = _AttachableImageWrapper(pointer: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } + /// Attach an image to the current test. + /// + /// - Parameters: + /// - image: The value to attach. + /// - 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`. + /// - sourceLocation: The source location of the call to this initializer. + /// This value is used when recording issues associated with the + /// attachment. + /// + /// This function creates a new instance of ``Attachment`` wrapping `image` + /// 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: + /// + /// - [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps) + /// - [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons) + /// + /// 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 + /// pass `nil`, then the image format that the testing library uses depends on + /// the path extension you specify in `preferredName`, if any. If you do not + /// 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. public static func record

( - _ image: P, + _ image: borrowing P, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper

{ - let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation) + let imageWrapper = _AttachableImageWrapper(pointer: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + 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 index ac08a111d..07a9c24aa 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -10,9 +10,9 @@ #if os(Windows) @_spi(Experimental) import Testing +private import _TestingInternals.GDIPlus -import WinSDK -private import _Gdiplus +internal import WinSDK enum GDIPlusError: Error { case status(Gdiplus.Status) @@ -47,12 +47,12 @@ func withGDIPlus(_ body: () throws -> R) throws -> R { var token = ULONG_PTR(0) var input = Gdiplus.GdiplusStartupInput(nil, false, false) - let rStartup = swt_winsdk_GdiplusStartup(&token, &input, nil) + let rStartup = swt_GdiplusStartup(&token, &input, nil) guard rStartup == Gdiplus.Ok else { throw GDIPlusError.status(rStartup) } defer { - swt_winsdk_GdiplusShutdown(token) + swt_GdiplusShutdown(token) } return try body() diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 64fcb64d1..7cd4e025c 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -10,28 +10,32 @@ #if os(Windows) @_spi(Experimental) public import Testing +private import _TestingInternals.GDIPlus -// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly public import WinSDK -private import _Gdiplus @_spi(Experimental) -extension WinSDK.HBITMAP__: AttachableAsGDIPlusImage { +extension HBITMAP__: AttachableAsGDIPlusImage { public static func _withGDIPlusImage( at address: P, - for attachment: borrowing Attachment>, + for attachment: borrowing Attachment & ~Copyable>, _ body: (OpaquePointer) throws -> R ) throws -> R where P: _Pointer, P.Pointee == Self { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address)) - guard let image = swt_winsdk_GdiplusBitmapCreate(address, nil) else { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + guard let bitmap = swt_GdiplusBitmapFromHBITMAP(address, nil) else { throw GDIPlusError.status(Gdiplus.GenericError) } defer { - swt_winsdk_GdiplusImageDelete(image) + swt_GdiplusBitmapDelete(bitmap) } return try withExtendedLifetime(self) { - try body(image) + try body(bitmap) } } + + public static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + DeleteObject(address) + } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index 47faa701f..395fd0106 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -10,28 +10,32 @@ #if os(Windows) @_spi(Experimental) public import Testing +private import _TestingInternals.GDIPlus -// FIXME: swiftc gets confused about the _Gdiplus module using types from WinSDK; needs to be part of WinSDK directly public import WinSDK -private import _Gdiplus @_spi(Experimental) -extension WinSDK.HICON__: AttachableAsGDIPlusImage { +extension HICON__: AttachableAsGDIPlusImage { public static func _withGDIPlusImage( at address: P, - for attachment: borrowing Attachment>, + for attachment: borrowing Attachment & ~Copyable>, _ body: (OpaquePointer) throws -> R ) throws -> R where P: _Pointer, P.Pointee == Self { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address)) - guard let image = swt_winsdk_GdiplusBitmapCreate(address) else { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + guard let bitmap = swt_GdiplusBitmapFromHICON(address) else { throw GDIPlusError.status(Gdiplus.GenericError) } defer { - swt_winsdk_GdiplusImageDelete(image) + swt_GdiplusBitmapDelete(bitmap) } return try withExtendedLifetime(self) { - try body(image) + try body(bitmap) } } + + public static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self { + let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + DeleteObject(address) + } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 1c79f3f75..717def5e7 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -10,19 +10,41 @@ #if os(Windows) @_spi(Experimental) public import Testing +private import _TestingInternals.GDIPlus -import WinSDK -private import _Gdiplus +internal import WinSDK @_spi(Experimental) -public struct _AttachableImageWrapper: Sendable where Pointer: _Pointer, Pointer.Pointee: AttachableAsGDIPlusImage { +public struct _AttachableImageWrapper: ~Copyable where Pointer: _Pointer, Pointer.Pointee: AttachableAsGDIPlusImage { /// A pointer to the underlying image. - nonisolated(unsafe) var pointer: Pointer + var pointer: Pointer /// The image format to use when encoding the represented image. var imageFormat: AttachableImageFormat? + + /// Whether or not to call `_cleanUpAttachment(at:)` on `pointer` when this + /// instance is deinitialized. + /// + /// - Note: If cleanup is not performed, `pointer` is effectively being + /// borrowed from the calling context. + var cleanUpWhenDone: Bool + + init(pointer: Pointer, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { + self.pointer = pointer + self.imageFormat = imageFormat + self.cleanUpWhenDone = cleanUpWhenDone + } + + deinit { + if cleanUpWhenDone { + Pointer.Pointee._cleanUpAttachment(at: pointer) + } + } } +@available(*, unavailable) +extension _AttachableImageWrapper: Sendable {} + // MARK: - @available(_uttypesAPI, *) @@ -37,11 +59,13 @@ extension _AttachableImageWrapper: AttachableWrapper { // does not provide a mechanism to access the underlying memory directly. var stream: UnsafeMutablePointer? let rCreateStream = CreateStreamOnHGlobal(nil, true, &stream) - guard S_OK == rCreateStream else { + guard S_OK == rCreateStream, let stream else { throw GDIPlusError.streamCreationFailed(rCreateStream) } defer { - swt_winsdk_IStreamRelease(stream) + stream.withMemoryRebound(to: IUnknown.self, capacity: 1) { stream in + _ = swt_IUnknown_Release(stream) + } } // Get the CLSID of the image encoder corresponding to the specified image @@ -54,7 +78,7 @@ extension _AttachableImageWrapper: AttachableWrapper { // Save the image into the stream. try pointer.withGDIPlusImage(for: attachment) { image in - let rSave = swt_winsdk_GdiplusImageSave(image, stream, &clsid, nil) + let rSave = swt_GdiplusBitmapSave(image, stream, &clsid, nil) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) } diff --git a/Sources/_Gdiplus/include/Includes.h b/Sources/_Gdiplus/include/Includes.h deleted file mode 100644 index ae6f5d68b..000000000 --- a/Sources/_Gdiplus/include/Includes.h +++ /dev/null @@ -1,16 +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_INCLUDES_H) -#define SWT_GDIPLUS_INCLUDES_H - -#include -#include -#endif diff --git a/Sources/_Gdiplus/include/Stubs.h b/Sources/_Gdiplus/include/Stubs.h deleted file mode 100644 index 12b1b2bdf..000000000 --- a/Sources/_Gdiplus/include/Stubs.h +++ /dev/null @@ -1,52 +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_STUBS_H) -#define SWT_GDIPLUS_STUBS_H - -#include "Includes.h" - -static inline Gdiplus::Status swt_winsdk_GdiplusStartup( - ULONG_PTR *token, - const Gdiplus::GdiplusStartupInput *input, - Gdiplus::GdiplusStartupOutput *output -) { - return Gdiplus::GdiplusStartup(token, input, output); -} - -static inline void swt_winsdk_GdiplusShutdown(ULONG_PTR token) { - Gdiplus::GdiplusShutdown(token); -} - -static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HBITMAP bitmap, HPALETTE palette) { - return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); -} - -static inline Gdiplus::Image *swt_winsdk_GdiplusBitmapCreate(HICON icon) { - return Gdiplus::Bitmap::FromHICON(icon); -} - -static inline void swt_winsdk_GdiplusImageDelete(Gdiplus::Image *image) { - delete image; -} - -static inline Gdiplus::Status swt_winsdk_GdiplusImageSave( - Gdiplus::Image *image, - IStream *stream, - const CLSID *format, - const Gdiplus::EncoderParameters *encoderParams -) { - return image->Save(stream, format, encoderParams); -} - -static inline void swt_winsdk_IStreamRelease(IStream *stream) { - stream->Release(); -} -#endif diff --git a/Sources/_Gdiplus/include/module.modulemap b/Sources/_Gdiplus/include/module.modulemap deleted file mode 100644 index 2d63bd95d..000000000 --- a/Sources/_Gdiplus/include/module.modulemap +++ /dev/null @@ -1,16 +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 -// - -module _Gdiplus { - umbrella "." - export * - - link "Gdiplus.lib" -} diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h new file mode 100644 index 000000000..f788ac2ee --- /dev/null +++ b/Sources/_TestingInternals/GDI+/include/GDI+.h @@ -0,0 +1,58 @@ +// +// 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 + +#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::Bitmap *_Nullable swt_GdiplusBitmapFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { + return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); +} + +static inline Gdiplus::Bitmap *_Nullable swt_GdiplusBitmapFromHICON(HICON icon) { + return Gdiplus::Bitmap::FromHICON(icon); +} + +static inline void swt_GdiplusBitmapDelete(Gdiplus::Bitmap *bitmap) { + delete bitmap; +} + +static inline Gdiplus::Status swt_GdiplusBitmapSave( + Gdiplus::Bitmap *bitmap, + IStream *stream, + const CLSID *format, + const Gdiplus::EncoderParameters *_Nullable encoderParams +) { + return bitmap->Save(stream, format, encoderParams); +} + +SWT_ASSUME_NONNULL_END + +#endif +#endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..ae641de0d 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -108,6 +108,24 @@ 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 e05a32552..cf5ea19e6 100644 --- a/Sources/_TestingInternals/include/module.modulemap +++ b/Sources/_TestingInternals/include/module.modulemap @@ -11,4 +11,13 @@ module _TestingInternals { umbrella "." export * + + explicit module GDIPlus { + header "../GDI+/include/GDI+.h" + export * + + requires cplusplus + + link "gdiplus.lib" + } } From 2aa478cd8de8df6a56dcdbe6412e0fe9fb682c68 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 12:49:18 -0400 Subject: [PATCH 10/25] Don't use _Pointer in the interface --- .../Attachments/AttachableAsGDIPlusImage.swift | 15 ++++++++------- .../Attachment+AttachableAsGDIPlusImage.swift | 16 ++++++++-------- .../HBITMAP+AttachableAsGDIPlusImage.swift | 11 +++++------ .../HICON+AttachableAsGDIPlusImage.swift | 11 +++++------ .../Attachments/_AttachableImageWrapper.swift | 16 ++++++++-------- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 1d6036a37..f8f65cbb6 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -14,6 +14,7 @@ private import _TestingInternals.GDIPlus internal import WinSDK +@_spi(Experimental) public protocol AttachableAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// @@ -40,11 +41,11 @@ public protocol AttachableAsGDIPlusImage { /// /// - Warning: Do not call this function directly. Instead, call /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. - static func _withGDIPlusImage( - at address: P, - for attachment: borrowing Attachment & ~Copyable>, + static func _withGDIPlusImage( + at address: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R where P: _Pointer, P.Pointee == Self + ) throws -> R /// Clean up any resources at the given address. /// @@ -56,10 +57,10 @@ public protocol AttachableAsGDIPlusImage { /// automatically by `_AttachableImageWrapper` when it is deinitialized. /// /// - Warning: Do not call this function directly. - static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self + static func _cleanUpAttachment(at address: UnsafeMutablePointer) } -extension _Pointer where Pointee: AttachableAsGDIPlusImage { +extension UnsafeMutablePointer where Pointee: AttachableAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// /// - Parameters: @@ -80,7 +81,7 @@ extension _Pointer where Pointee: AttachableAsGDIPlusImage { /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. func withGDIPlusImage( - for attachment: borrowing Attachment & ~Copyable>, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R ) throws -> R { // Stuff the attachment into a pointer so we can reference it from within diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift index 0ea55e389..e5b3a4058 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -47,13 +47,13 @@ extension Attachment where AttachableValue: ~Copyable { /// ``Attachment/record(_:named:as:sourceLocation)`` instead of this /// initializer, or make a copy of the resource before passing it to this /// initializer. - public init

( - _ attachableValue: consuming P, + public init( + _ attachableValue: consuming UnsafeMutablePointer, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper

{ - let imageWrapper = _AttachableImageWrapper(pointer: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(imageAddress: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -85,13 +85,13 @@ 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. - public static func record

( - _ image: borrowing P, + public static func record( + _ image: borrowing UnsafeMutablePointer, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation - ) where AttachableValue == _AttachableImageWrapper

{ - let imageWrapper = _AttachableImageWrapper(pointer: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + ) where AttachableValue == _AttachableImageWrapper { + let imageWrapper = _AttachableImageWrapper(imageAddress: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 7cd4e025c..37db2e753 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -16,12 +16,11 @@ public import WinSDK @_spi(Experimental) extension HBITMAP__: AttachableAsGDIPlusImage { - public static func _withGDIPlusImage( - at address: P, - for attachment: borrowing Attachment & ~Copyable>, + public static func _withGDIPlusImage( + at address: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R where P: _Pointer, P.Pointee == Self { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + ) throws -> R { guard let bitmap = swt_GdiplusBitmapFromHBITMAP(address, nil) else { throw GDIPlusError.status(Gdiplus.GenericError) } @@ -33,7 +32,7 @@ extension HBITMAP__: AttachableAsGDIPlusImage { } } - public static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self { + public static func _cleanUpAttachment(at address: UnsafeMutablePointer) { let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! DeleteObject(address) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index 395fd0106..d05e221b0 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -16,12 +16,11 @@ public import WinSDK @_spi(Experimental) extension HICON__: AttachableAsGDIPlusImage { - public static func _withGDIPlusImage( - at address: P, - for attachment: borrowing Attachment & ~Copyable>, + public static func _withGDIPlusImage( + at address: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R where P: _Pointer, P.Pointee == Self { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! + ) throws -> R { guard let bitmap = swt_GdiplusBitmapFromHICON(address) else { throw GDIPlusError.status(Gdiplus.GenericError) } @@ -33,7 +32,7 @@ extension HICON__: AttachableAsGDIPlusImage { } } - public static func _cleanUpAttachment

(at address: P) where P: _Pointer, P.Pointee == Self { + public static func _cleanUpAttachment(at address: UnsafeMutablePointer) { let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! DeleteObject(address) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 717def5e7..7d82a0c40 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -15,9 +15,9 @@ private import _TestingInternals.GDIPlus internal import WinSDK @_spi(Experimental) -public struct _AttachableImageWrapper: ~Copyable where Pointer: _Pointer, Pointer.Pointee: AttachableAsGDIPlusImage { +public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsGDIPlusImage { /// A pointer to the underlying image. - var pointer: Pointer + var imageAddress: UnsafeMutablePointer /// The image format to use when encoding the represented image. var imageFormat: AttachableImageFormat? @@ -29,15 +29,15 @@ public struct _AttachableImageWrapper: ~Copyable where Pointer: _Pointe /// borrowed from the calling context. var cleanUpWhenDone: Bool - init(pointer: Pointer, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { - self.pointer = pointer + init(imageAddress: UnsafeMutablePointer, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { + self.imageAddress = imageAddress self.imageFormat = imageFormat self.cleanUpWhenDone = cleanUpWhenDone } deinit { if cleanUpWhenDone { - Pointer.Pointee._cleanUpAttachment(at: pointer) + Image._cleanUpAttachment(at: imageAddress) } } } @@ -49,8 +49,8 @@ extension _AttachableImageWrapper: Sendable {} @available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: Pointer { - pointer + public var wrappedValue: UnsafePointer { + UnsafePointer(imageAddress) } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { @@ -77,7 +77,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } // Save the image into the stream. - try pointer.withGDIPlusImage(for: attachment) { image in + try imageAddress.withGDIPlusImage(for: attachment) { image in let rSave = swt_GdiplusBitmapSave(image, stream, &clsid, nil) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) From af7086e1931ec1c516007402677640dbc93e2b8c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 12:59:54 -0400 Subject: [PATCH 11/25] Make UnsafeMutablePointer recursively conformant to AttachableAsGDIPlusImage so that not all attachable images are forced to be UnsafeMutablePointer --- .../AttachableAsGDIPlusImage.swift | 39 ++++++++++--------- .../HBITMAP+AttachableAsGDIPlusImage.swift | 15 ++++--- .../HICON+AttachableAsGDIPlusImage.swift | 15 ++++--- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index f8f65cbb6..14557765b 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -19,7 +19,7 @@ public protocol AttachableAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// /// - Parameters: - /// - address: The address of the instance of this type. + /// - imageAddress: The address of the instance of this type. /// - attachment: The attachment that is requesting an image (that is, the /// attachment containing this instance.) /// - body: A function to call. A copy of this instance converted to a GDI+ @@ -41,26 +41,26 @@ public protocol AttachableAsGDIPlusImage { /// /// - Warning: Do not call this function directly. Instead, call /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. - static func _withGDIPlusImage( - at address: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, + static func _withGDIPlusImage( + at imageAddress: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R + ) throws -> R where A: AttachableAsGDIPlusImage /// Clean up any resources at the given address. /// /// - Parameters: - /// - address: The address of the instance of this type. + /// - imageAddress: The address of the instance of this type. /// /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) at `address`. This function is invoked + /// handles or COM objects) at `imageAddress`. This function is invoked /// automatically by `_AttachableImageWrapper` when it is deinitialized. /// /// - Warning: Do not call this function directly. - static func _cleanUpAttachment(at address: UnsafeMutablePointer) + static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) } -extension UnsafeMutablePointer where Pointee: AttachableAsGDIPlusImage { +extension AttachableAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// /// - Parameters: @@ -80,16 +80,19 @@ extension UnsafeMutablePointer where Pointee: AttachableAsGDIPlusImage { /// - Warning: GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) /// by design. The caller is responsible for guarding against concurrent /// access to the resulting GDI+ image object. - func withGDIPlusImage( - for attachment: borrowing Attachment<_AttachableImageWrapper>, + func withGDIPlusImage( + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R { - // Stuff the attachment into a pointer so we can reference it from within - // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't - // reason about the lifetime of a borrowed value passed into a closure.) - try withUnsafePointer(to: attachment) { attachment in - try withGDIPlus { - try Pointee._withGDIPlusImage(at: self, for: attachment.pointee, body) + ) throws -> R where A: AttachableAsGDIPlusImage { + var selfCopy = self + return try withUnsafeMutablePointer(to: &selfCopy) { imageAddress in + // Stuff the attachment into a pointer so we can reference it from within + // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't + // reason about the lifetime of a borrowed value passed into a closure.) + try withUnsafePointer(to: attachment) { attachment in + try withGDIPlus { + try Self._withGDIPlusImage(at: imageAddress, for: attachment.pointee, body) + } } } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 37db2e753..e6c79df11 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -16,12 +16,12 @@ public import WinSDK @_spi(Experimental) extension HBITMAP__: AttachableAsGDIPlusImage { - public static func _withGDIPlusImage( - at address: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, + public static func _withGDIPlusImage( + at imageAddress: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R { - guard let bitmap = swt_GdiplusBitmapFromHBITMAP(address, nil) else { + ) throws -> R where A: AttachableAsGDIPlusImage { + guard let bitmap = swt_GdiplusBitmapFromHBITMAP(imageAddress, nil) else { throw GDIPlusError.status(Gdiplus.GenericError) } defer { @@ -32,9 +32,8 @@ extension HBITMAP__: AttachableAsGDIPlusImage { } } - public static func _cleanUpAttachment(at address: UnsafeMutablePointer) { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! - DeleteObject(address) + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index d05e221b0..f77fc7174 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -16,12 +16,12 @@ public import WinSDK @_spi(Experimental) extension HICON__: AttachableAsGDIPlusImage { - public static func _withGDIPlusImage( - at address: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, + public static func _withGDIPlusImage( + at imageAddress: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R - ) throws -> R { - guard let bitmap = swt_GdiplusBitmapFromHICON(address) else { + ) throws -> R where A: AttachableAsGDIPlusImage { + guard let bitmap = swt_GdiplusBitmapFromHICON(imageAddress) else { throw GDIPlusError.status(Gdiplus.GenericError) } defer { @@ -32,9 +32,8 @@ extension HICON__: AttachableAsGDIPlusImage { } } - public static func _cleanUpAttachment(at address: UnsafeMutablePointer) { - let address = UnsafeMutablePointer(bitPattern: UInt(bitPattern: address))! - DeleteObject(address) + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + DeleteObject(imageAddress) } } #endif From c2e3f04cc437f5f59be19211b0c7a47019596157 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 14:03:52 -0400 Subject: [PATCH 12/25] Missing a file, sigh --- ...ablePointer+AttachableAsGDIPlusImage.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift new file mode 100644 index 000000000..268e94d40 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -0,0 +1,28 @@ +// +// 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) public import Testing + +@_spi(Experimental) +extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: AttachableAsGDIPlusImage{ + public static func _withGDIPlusImage( + at imageAddress: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, + _ body: (OpaquePointer) throws -> R + ) throws -> R where A: AttachableAsGDIPlusImage { + try Pointee._withGDIPlusImage(at: imageAddress.pointee, for: attachment, body) + } + + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + Pointee._cleanUpAttachment(at: imageAddress.pointee) + } +} +#endif From 2ec1708519004b040a8aea4aaa09ec1e563bcdae Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 16:38:54 -0400 Subject: [PATCH 13/25] Get type inference working --- .../AttachableAsGDIPlusImage.swift | 101 +++++++++++--- .../AttachableImageFormat+CLSID.swift | 125 ++++++++++++++++-- .../Attachment+AttachableAsGDIPlusImage.swift | 9 +- .../HBITMAP+AttachableAsGDIPlusImage.swift | 2 +- .../HICON+AttachableAsGDIPlusImage.swift | 2 +- ...ablePointer+AttachableAsGDIPlusImage.swift | 11 +- .../Attachments/_AttachableImageWrapper.swift | 53 ++++++-- Sources/_TestingInternals/GDI+/include/GDI+.h | 4 + .../include/module.modulemap | 2 - Tests/TestingTests/AttachmentTests.swift | 55 +++++++- 10 files changed, 303 insertions(+), 61 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 14557765b..967dc3d72 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -14,8 +14,25 @@ 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 AttachableAsGDIPlusImage { +public protocol _AttachableByAddressAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// /// - Parameters: @@ -35,12 +52,8 @@ public protocol AttachableAsGDIPlusImage { /// can therefore assume that GDI+ is correclty configured on the current /// thread when it is called. /// - /// - Warning: GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) - /// by design. The caller is responsible for guarding against concurrent - /// access to the resulting GDI+ image object. - /// /// - Warning: Do not call this function directly. Instead, call - /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. + /// ``AttachableAsGDIPlusImage/withGDIPlusImage(for:_:)``. static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, @@ -60,6 +73,63 @@ public protocol AttachableAsGDIPlusImage { 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 { + /// Call a function and pass a GDI+ image representing this instance to it. + /// + /// - Parameters: + /// - attachment: The attachment that is requesting an image (that is, the + /// attachment containing this instance.) + /// - body: A function to call. A copy of this instance converted to a GDI+ + /// image is passed to it. + /// + /// - Returns: Whatever is returned by `body`. + /// + /// - Throws: Whatever is thrown by `body`, or any error that prevented the + /// creation of the buffer. + /// + /// The testing library automatically calls `GdiplusStartup()` and + /// `GdiplusShutdown()` before and after calling this function. This function + /// can therefore assume that GDI+ is correclty configured on the current + /// thread when it is called. + /// + /// - Warning: Do not call this function directly. Instead, call + /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. + func _withGDIPlusImage( + for attachment: borrowing Attachment<_AttachableImageWrapper>, + _ body: (OpaquePointer) throws -> R + ) throws -> R where A: AttachableAsGDIPlusImage + + /// 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) at `imageAddress`. This function is invoked + /// automatically by `_AttachableImageWrapper` when it is deinitialized. + /// + /// - Warning: Do not call this function directly. + func _cleanUpAttachment() +} + extension AttachableAsGDIPlusImage { /// Call a function and pass a GDI+ image representing this instance to it. /// @@ -76,23 +146,16 @@ extension AttachableAsGDIPlusImage { /// /// This function is a convenience wrapper around `_withGDIPlusImage()` that /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. - /// - /// - Warning: GDI+ objects are [not thread-safe](https://learn.microsoft.com/en-us/windows/win32/procthread/multiple-threads-and-gdi-objects) - /// by design. The caller is responsible for guarding against concurrent - /// access to the resulting GDI+ image object. func withGDIPlusImage( for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { - var selfCopy = self - return try withUnsafeMutablePointer(to: &selfCopy) { imageAddress in - // Stuff the attachment into a pointer so we can reference it from within - // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't - // reason about the lifetime of a borrowed value passed into a closure.) - try withUnsafePointer(to: attachment) { attachment in - try withGDIPlus { - try Self._withGDIPlusImage(at: imageAddress, for: attachment.pointee, body) - } + // Stuff the attachment into a pointer so we can reference it from within + // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't + // reason about the lifetime of a borrowed value passed into a closure.) + try withUnsafePointer(to: attachment) { attachment in + try withGDIPlus { + try _withGDIPlusImage(for: attachment.pointee, body) } } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 198fe1eed..5bdbb051b 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -19,12 +19,12 @@ extension AttachableImageFormat { /// /// If the testing library was unable to determine the set of image formats, /// the value of this property is `nil`. - private static nonisolated(unsafe) let _allImageCodecInfo: UnsafeBufferPointer? = { + private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer? = { try? withGDIPlus { // Find out the size of the buffer needed. - var encoderCount = UINT(0) + var codecCount = UINT(0) var byteCount = UINT(0) - let rGetSize = Gdiplus.GetImageEncodersSize(&encoderCount, &byteCount) + let rGetSize = Gdiplus.GetImageEncodersSize(&codecCount, &byteCount) guard rGetSize == Gdiplus.Ok else { return nil } @@ -36,20 +36,129 @@ extension AttachableImageFormat { byteCount: Int(byteCount), alignment: MemoryLayout.alignment ) - let encoderBuffer = result - .prefix(MemoryLayout.stride * Int(encoderCount)) + let codecBuffer = result + .prefix(MemoryLayout.stride * Int(codecCount)) .bindMemory(to: Gdiplus.ImageCodecInfo.self) // Read the encoders list. - let rGetEncoders = Gdiplus.GetImageEncoders(encoderCount, byteCount, encoderBuffer.baseAddress!) + let rGetEncoders = Gdiplus.GetImageEncoders(codecCount, byteCount, codecBuffer.baseAddress!) guard rGetEncoders == Gdiplus.Ok else { result.deallocate() return nil } - return .init(encoderBuffer) + return .init(codecBuffer) } }() + /// Get the set of path extensions corresponding to the image format + /// represented by a GDI+ codec info structure. + /// + /// - Parameters: + /// - codec: The GDI+ codec info structure 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 { + return [] + } + return extensions + .split(separator: ";") + .map { ext in + if ext.starts(with: "*.") { + ext.dropFirst(2) + } else { + ext[...] + } + }.map{ $0.lowercased() } // Vestiges of MS-DOS... + } + + /// Get the `CLSID` value 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 + /// `nil` if one could not be determined. + private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { + preferredName.withCString(encodedAs: UTF16.self) { (preferredName) -> CLSID? in + // Get the path extension on the preferred name, if any. + var dot: PCWSTR? + guard S_OK == PathCchFindExtension(preferredName, wcslen(preferredName) + 1, &dot), let dot, dot[0] != 0 else { + return nil + } + let ext = dot + 1 + + return _allCodecs?.first { codec in + _pathExtensions(for: codec) + .contains { codecExtension in + codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in + 0 == _wcsicmp(ext, codecExtension) + } + } + }.map(\.Clsid) + } + } + + /// Get the `CLSID` value` to use when encoding the image. + /// + /// - Parameters: + /// - imageFormat: The image format to use, or `nil` if the developer did + /// not specify one. + /// - 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. + /// + /// This function is not part of the public interface of the testing library. + static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID? { + if let clsid = imageFormat?.clsid { + return clsid + } + + // The developer didn't specify a CLSID, or we couldn't figure one out from + // context, so try to derive one from the preferred name's path extension. + if let inferredCLSID = _computeCLSID(forPreferredName: preferredName) { + return inferredCLSID + } + + // 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 + } + + /// Append the path extension preferred by GDI+ for the given `CLSID` value + /// representing an image format to a suggested extension 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. + 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. + if let existingCLSID = _computeCLSID(forPreferredName: preferredName), 0 != IsEqualGUID(clsid, existingCLSID) { + 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 + } + + return "\(preferredName).\(ext)" + } + /// Get a `CLSID` value corresponding to the image format with the given MIME /// type. /// @@ -60,7 +169,7 @@ extension AttachableImageFormat { /// was found corresponding to `mimeType`. private static func _clsid(forMIMEType mimeType: String) -> CLSID? { mimeType.withCString(encodedAs: UTF16.self) { mimeType in - _allImageCodecInfo?.first { 0 == wcscmp($0.MimeType, mimeType) }?.Clsid + _allCodecs?.first { 0 == wcscmp($0.MimeType, mimeType) }?.Clsid } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift index e5b3a4058..e51a11275 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -47,13 +47,14 @@ extension Attachment where AttachableValue: ~Copyable { /// ``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 UnsafeMutablePointer, + _ attachableValue: consuming T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(imageAddress: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat, cleanUpWhenDone: true) self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation) } @@ -86,12 +87,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 static func record( - _ image: borrowing UnsafeMutablePointer, + _ image: borrowing T, named preferredName: String? = nil, as imageFormat: AttachableImageFormat? = nil, sourceLocation: SourceLocation = #_sourceLocation ) where AttachableValue == _AttachableImageWrapper { - let imageWrapper = _AttachableImageWrapper(imageAddress: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) + let imageWrapper = _AttachableImageWrapper(image: copy image, imageFormat: imageFormat, cleanUpWhenDone: true) let attachment = Self(imageWrapper, named: preferredName, sourceLocation: sourceLocation) Self.record(attachment, sourceLocation: sourceLocation) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index e6c79df11..44a04e462 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -15,7 +15,7 @@ private import _TestingInternals.GDIPlus public import WinSDK @_spi(Experimental) -extension HBITMAP__: AttachableAsGDIPlusImage { +extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { public static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index f77fc7174..9d5b68fad 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -15,7 +15,7 @@ private import _TestingInternals.GDIPlus public import WinSDK @_spi(Experimental) -extension HICON__: AttachableAsGDIPlusImage { +extension HICON__: _AttachableByAddressAsGDIPlusImage { public static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift index 268e94d40..fc4bc036f 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -12,17 +12,16 @@ @_spi(Experimental) public import Testing @_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: AttachableAsGDIPlusImage{ - public static func _withGDIPlusImage( - at imageAddress: UnsafeMutablePointer, +extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { + public func _withGDIPlusImage( for attachment: borrowing Attachment<_AttachableImageWrapper>, _ body: (OpaquePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { - try Pointee._withGDIPlusImage(at: imageAddress.pointee, for: attachment, body) + try Pointee._withGDIPlusImage(at: self, for: attachment, body) } - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - Pointee._cleanUpAttachment(at: imageAddress.pointee) + public func _cleanUpAttachment() { + Pointee._cleanUpAttachment(at: self) } } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 7d82a0c40..1a3fced95 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -14,10 +14,21 @@ private import _TestingInternals.GDIPlus internal import WinSDK +/// A wrapper type for image types such as `HBITMAP` and `HICON` that can be +/// attached indirectly. +/// +/// You do not need to use this type directly. Instead, initialize an instance +/// of ``Attachment`` using an instance of an image type that conforms to +/// ``AttachableAsGDIPlusImage``. The following system-provided image types +/// conform to the ``AttachableAsGDIPlusImage`` 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) @_spi(Experimental) public struct _AttachableImageWrapper: ~Copyable where Image: AttachableAsGDIPlusImage { - /// A pointer to the underlying image. - var imageAddress: UnsafeMutablePointer + /// The underlying image. + var image: Image /// The image format to use when encoding the represented image. var imageFormat: AttachableImageFormat? @@ -29,15 +40,15 @@ public struct _AttachableImageWrapper: ~Copyable where Image: AttachableA /// borrowed from the calling context. var cleanUpWhenDone: Bool - init(imageAddress: UnsafeMutablePointer, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { - self.imageAddress = imageAddress + init(image: Image, imageFormat: AttachableImageFormat?, cleanUpWhenDone: Bool) { + self.image = image self.imageFormat = imageFormat self.cleanUpWhenDone = cleanUpWhenDone } deinit { if cleanUpWhenDone { - Image._cleanUpAttachment(at: imageAddress) + image._cleanUpAttachment() } } } @@ -49,8 +60,8 @@ extension _AttachableImageWrapper: Sendable {} @available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { - public var wrappedValue: UnsafePointer { - UnsafePointer(imageAddress) + public var wrappedValue: Image { + image } public func withUnsafeBytes(for attachment: borrowing Attachment, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R { @@ -70,17 +81,26 @@ extension _AttachableImageWrapper: AttachableWrapper { // Get the CLSID of the image encoder corresponding to the specified image // format. - // TODO: infer an image format from the filename like we do on Darwin. let imageFormat = self.imageFormat ?? .png guard var clsid = imageFormat.clsid else { throw GDIPlusError.clsidNotFound } - // Save the image into the stream. - try imageAddress.withGDIPlusImage(for: attachment) { image in - let rSave = swt_GdiplusBitmapSave(image, stream, &clsid, nil) - guard rSave == Gdiplus.Ok else { - throw GDIPlusError.status(rSave) + var encodingQuality = LONG(imageFormat.encodingQuality * 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. + try image.withGDIPlusImage(for: attachment) { image in + let rSave = swt_GdiplusBitmapSave(image, stream, &clsid, &encoderParams) + guard rSave == Gdiplus.Ok else { + throw GDIPlusError.status(rSave) + } } } @@ -101,5 +121,12 @@ extension _AttachableImageWrapper: AttachableWrapper { let byteCount = GlobalSize(global) return try body(UnsafeRawBufferPointer(start: baseAddress, count: Int(byteCount))) } + + public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { + guard let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) else { + return suggestedName + } + return AttachableImageFormat.appendPathExtension(for: clsid, to: suggestedName) + } } #endif diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h index f788ac2ee..b1efa76fd 100644 --- a/Sources/_TestingInternals/GDI+/include/GDI+.h +++ b/Sources/_TestingInternals/GDI+/include/GDI+.h @@ -52,6 +52,10 @@ static inline Gdiplus::Status swt_GdiplusBitmapSave( return bitmap->Save(stream, format, encoderParams); } +static inline GUID swt_GdiplusEncoderQuality(void) { + return Gdiplus::EncoderQuality; +} + SWT_ASSUME_NONNULL_END #endif diff --git a/Sources/_TestingInternals/include/module.modulemap b/Sources/_TestingInternals/include/module.modulemap index cf5ea19e6..12a23c81d 100644 --- a/Sources/_TestingInternals/include/module.modulemap +++ b/Sources/_TestingInternals/include/module.modulemap @@ -16,8 +16,6 @@ module _TestingInternals { header "../GDI+/include/GDI+.h" export * - requires cplusplus - link "gdiplus.lib" } } diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index e5f7b3d47..99dd7c39c 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -35,7 +35,7 @@ import UniformTypeIdentifiers #endif #if canImport(WinSDK) && canImport(_Testing_WinSDK) import WinSDK -@_spi(Experimental) import _Testing_WinSDK +@testable @_spi(Experimental) import _Testing_WinSDK #endif @Suite("Attachment Tests") @@ -696,24 +696,32 @@ extension AttachmentTests { try attachment.attachableValue.withUnsafeBytes(for: attachment) { buffer in #expect(buffer.count > 32) } + Attachment.record(attachment) } #endif #endif #if canImport(WinSDK) && canImport(_Testing_WinSDK) + private func copyHICON() throws -> HICON { + try #require(LoadIconA(nil, swt_IDI_SHIELD())) + } + @MainActor @Test func attachHICON() throws { - let icon = try #require(LoadIconA(nil, swt_IDI_SHIELD())) - let attachment = Attachment(icon, named: "square.png") + let icon = try copyHICON() + defer { + DeleteObject(icon) + } + + let attachment = Attachment(icon, named: "diamond.jpeg") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) } - Attachment.record(attachment) } - @MainActor @Test func attachHBITMAP() throws { + private func copyHBITMAP() throws -> HBITMAP { let (width, height) = (GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) - let icon = try #require(LoadIconA(nil, swt_IDI_SHIELD())) + let icon = try copyHICON() defer { DeleteObject(icon) } @@ -732,11 +740,44 @@ extension AttachmentTests { SelectObject(dc, bitmap) DrawIcon(dc, 0, 0, icon) + return bitmap + } + + @MainActor @Test func attachHBITMAP() throws { + let bitmap = try copyHBITMAP() let attachment = Attachment(bitmap, named: "diamond.png") try attachment.withUnsafeBytes { buffer in #expect(buffer.count > 32) } - Attachment.record(attachment) + } + + @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)) + try hiFi.withUnsafeBytes { hiFi in + try loFi.withUnsafeBytes { loFi in + #expect(hiFi.count > loFi.count) + } + } + Attachment.record(loFi) + } + + @MainActor @Test func pathExtensionAndCLSID() throws { + let pngCLSID = try #require(AttachableImageFormat.png.clsid) + let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") + #expect(pngFilename == "example.png") + + let jpegCLSID = try #require(AttachableImageFormat.jpeg.clsid) + let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") + #expect(jpegFilename == "example.jpg") + + let pngjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.png") + #expect(pngjpegFilename == "example.png.jpg") + + let jpgjpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example.jpeg") + #expect(jpgjpegFilename == "example.jpeg") } #endif } From fd224062808cf7813c3a55c65b785e1a18d6da42 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 18:58:15 -0400 Subject: [PATCH 14/25] Define an abstraction over OpaquePointer to use until Gdiplus.Image can be referenced directly from Swift --- .../Attachments/AttachableAsGDIPlusImage.swift | 8 ++++---- .../_Testing_WinSDK/Attachments/GDI+.swift | 2 +- .../HBITMAP+AttachableAsGDIPlusImage.swift | 11 +++++------ .../HICON+AttachableAsGDIPlusImage.swift | 11 +++++------ ...tablePointer+AttachableAsGDIPlusImage.swift | 4 ++-- .../Attachments/_AttachableImageWrapper.swift | 2 +- Sources/_TestingInternals/GDI+/include/GDI+.h | 18 +++++++++++------- Tests/TestingTests/AttachmentTests.swift | 5 ++++- 8 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 967dc3d72..b6c4d70a2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -32,7 +32,7 @@ internal import WinSDK /// 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 { +public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// Call a function and pass a GDI+ image representing this instance to it. /// /// - Parameters: @@ -57,7 +57,7 @@ public protocol _AttachableByAddressAsGDIPlusImage { static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage /// Clean up any resources at the given address. @@ -114,7 +114,7 @@ public protocol AttachableAsGDIPlusImage { /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. func _withGDIPlusImage( for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage /// Clean up any resources at the given address. @@ -148,7 +148,7 @@ extension AttachableAsGDIPlusImage { /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. func withGDIPlusImage( for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { // Stuff the attachment into a pointer so we can reference it from within // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index 07a9c24aa..9188705d3 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -10,7 +10,7 @@ #if os(Windows) @_spi(Experimental) import Testing -private import _TestingInternals.GDIPlus +internal import _TestingInternals.GDIPlus internal import WinSDK diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 44a04e462..5b9cdd045 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -19,16 +19,15 @@ extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { public static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { - guard let bitmap = swt_GdiplusBitmapFromHBITMAP(imageAddress, nil) else { - throw GDIPlusError.status(Gdiplus.GenericError) - } + let image = swt_GdiplusImageFromHBITMAP(imageAddress, nil) defer { - swt_GdiplusBitmapDelete(bitmap) + swt_GdiplusImageDelete(image) } return try withExtendedLifetime(self) { - try body(bitmap) + var image: GDIPlusImage = GDIPlusImage(borrowing: image) + return try body(&image) } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index 9d5b68fad..4064bd0fa 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -19,16 +19,15 @@ extension HICON__: _AttachableByAddressAsGDIPlusImage { public static func _withGDIPlusImage( at imageAddress: UnsafeMutablePointer, for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { - guard let bitmap = swt_GdiplusBitmapFromHICON(imageAddress) else { - throw GDIPlusError.status(Gdiplus.GenericError) - } + let image = swt_GdiplusImageFromHICON(imageAddress) defer { - swt_GdiplusBitmapDelete(bitmap) + swt_GdiplusImageDelete(image) } return try withExtendedLifetime(self) { - try body(bitmap) + var image = GDIPlusImage(borrowing: image) + return try body(&image) } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift index fc4bc036f..5b52ff6ab 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -12,10 +12,10 @@ @_spi(Experimental) public import Testing @_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { +extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage & ~Copyable { public func _withGDIPlusImage( for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (OpaquePointer) throws -> R + _ body: (borrowing UnsafeMutablePointer) throws -> R ) throws -> R where A: AttachableAsGDIPlusImage { try Pointee._withGDIPlusImage(at: self, for: attachment, body) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 1a3fced95..1c800dc4d 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -97,7 +97,7 @@ extension _AttachableImageWrapper: AttachableWrapper { // Save the image into the stream. try image.withGDIPlusImage(for: attachment) { image in - let rSave = swt_GdiplusBitmapSave(image, stream, &clsid, &encoderParams) + let rSave = swt_GdiplusImageSave(image.pointee.imageAddress, stream, &clsid, &encoderParams) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) } diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h index b1efa76fd..4b510f1b2 100644 --- a/Sources/_TestingInternals/GDI+/include/GDI+.h +++ b/Sources/_TestingInternals/GDI+/include/GDI+.h @@ -31,25 +31,29 @@ static inline void swt_GdiplusShutdown(ULONG_PTR token) { Gdiplus::GdiplusShutdown(token); } -static inline Gdiplus::Bitmap *_Nullable swt_GdiplusBitmapFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { +static inline Gdiplus::Image *swt_GdiplusImageFromHBITMAP(HBITMAP bitmap, HPALETTE _Nullable palette) { return Gdiplus::Bitmap::FromHBITMAP(bitmap, palette); } -static inline Gdiplus::Bitmap *_Nullable swt_GdiplusBitmapFromHICON(HICON icon) { +static inline Gdiplus::Image *swt_GdiplusImageFromHICON(HICON icon) { return Gdiplus::Bitmap::FromHICON(icon); } -static inline void swt_GdiplusBitmapDelete(Gdiplus::Bitmap *bitmap) { - delete bitmap; +static inline Gdiplus::Image *swt_GdiplusImageClone(Gdiplus::Image *image) { + return image->Clone(); } -static inline Gdiplus::Status swt_GdiplusBitmapSave( - Gdiplus::Bitmap *bitmap, +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 bitmap->Save(stream, format, encoderParams); + return image->Save(stream, format, encoderParams); } static inline GUID swt_GdiplusEncoderQuality(void) { diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index 99dd7c39c..ae544d87b 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -737,7 +737,10 @@ extension AttachmentTests { } let bitmap = try #require(CreateCompatibleBitmap(screenDC, width, height)) - SelectObject(dc, bitmap) + let oldSelectedObject = SelectObject(dc, bitmap) + defer { + _ = SelectObject(dc, oldSelectedObject) + } DrawIcon(dc, 0, 0, icon) return bitmap From 16c9e0260dc00d02e3d9bf6440db403cb85f9a16 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 18:59:52 -0400 Subject: [PATCH 15/25] Stop forgetting my files! --- .../Attachments/GDIPlusImage.swift | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift new file mode 100644 index 000000000..efde371c3 --- /dev/null +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift @@ -0,0 +1,81 @@ +// +// 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 GDI+ image. +/// +/// Instances of this type represent GDI+ images (that is, instances of +/// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). +@_spi(Experimental) +@unsafe public struct GDIPlusImage: ~Copyable { + /// The address of the C++ `Gdiplus::Image` instance. + var imageAddress: OpaquePointer + + private var _deleteWhenDone: Bool + + /// Construct an instance of this type by cloning an existing GDI+ image. + /// + /// - Parameters: + /// - imageAddress: The address of an existing GDI+ image of type + /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). + /// + /// This initializer makes a copy of `imageAddress` by calling its[`Clone()`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nf-gdiplusheaders-image-clone) + /// function. The caller is responsible for ensuring that the resources + /// backing the resulting image remain valid until it is deinitialized. + /// + /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the + /// result is undefined. + public init(unsafe imageAddress: OpaquePointer) { + self.imageAddress = swt_GdiplusImageClone(imageAddress) + self._deleteWhenDone = true + } + + /// Construct an instance of this type by borrowing an existing GDI+ image. + /// + /// - Parameters: + /// - imageAddress: The address of an existing GDI+ image of type + /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). + /// + /// The caller is responsible for ensuring that the resources backing the + /// resulting image remain valid until it is deinitialized. + /// + /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the + /// result is undefined. + init(borrowing imageAddress: OpaquePointer) { + self.imageAddress = imageAddress + self._deleteWhenDone = false + } + + deinit { + if _deleteWhenDone { + swt_GdiplusImageDelete(imageAddress) + } + } +} + +extension GDIPlusImage: _AttachableByAddressAsGDIPlusImage { + public static func _withGDIPlusImage( + at imageAddress: UnsafeMutablePointer, + for attachment: borrowing Attachment<_AttachableImageWrapper>, + _ body: (borrowing UnsafeMutablePointer) throws -> R + ) throws -> R where A: AttachableAsGDIPlusImage { + try body(imageAddress) + } + + public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { + swt_GdiplusImageDelete(imageAddress.pointee.imageAddress) + } +} +#endif From 97c7150e649236357ab4bf97d5fa3a457290e5f2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Wed, 30 Jul 2025 19:03:07 -0400 Subject: [PATCH 16/25] Fix typo --- .../Attachments/AttachableAsGDIPlusImage.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index b6c4d70a2..87a062623 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -49,7 +49,7 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// /// The testing library automatically calls `GdiplusStartup()` and /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correclty configured on the current + /// can therefore assume that GDI+ is correctly configured on the current /// thread when it is called. /// /// - Warning: Do not call this function directly. Instead, call @@ -107,7 +107,7 @@ public protocol AttachableAsGDIPlusImage { /// /// The testing library automatically calls `GdiplusStartup()` and /// `GdiplusShutdown()` before and after calling this function. This function - /// can therefore assume that GDI+ is correclty configured on the current + /// can therefore assume that GDI+ is correctly configured on the current /// thread when it is called. /// /// - Warning: Do not call this function directly. Instead, call From 4750c9df6d1255b8128e0fdf330e877f4f74d109 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 08:02:03 -0400 Subject: [PATCH 17/25] Work around SIL crash, add init(pathExtension:), fix some whitespace issues --- .../AttachableAsGDIPlusImage.swift | 16 ++-- .../AttachableImageFormat+CLSID.swift | 93 ++++++++++++++----- .../Attachments/GDIPlusImage.swift | 14 +-- .../Attachments/_AttachableImageWrapper.swift | 2 +- Tests/TestingTests/AttachmentTests.swift | 2 +- 5 files changed, 89 insertions(+), 38 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 87a062623..200530b84 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -46,7 +46,7 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. - /// + /// /// 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 @@ -61,14 +61,14 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { ) throws -> R where A: AttachableAsGDIPlusImage /// 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) at `imageAddress`. This function is invoked /// automatically by `_AttachableImageWrapper` when it is deinitialized. - /// + /// /// - Warning: Do not call this function directly. static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) } @@ -104,7 +104,7 @@ public protocol AttachableAsGDIPlusImage { /// /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. - /// + /// /// 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 @@ -118,14 +118,14 @@ public protocol AttachableAsGDIPlusImage { ) throws -> R where A: AttachableAsGDIPlusImage /// 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) at `imageAddress`. This function is invoked /// automatically by `_AttachableImageWrapper` when it is deinitialized. - /// + /// /// - Warning: Do not call this function directly. func _cleanUpAttachment() } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 5bdbb051b..09a2d6dc2 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -16,7 +16,7 @@ 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`. private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer? = { @@ -52,10 +52,10 @@ extension AttachableImageFormat { /// Get the set of path extensions corresponding to the image format /// represented by a GDI+ codec info structure. - /// + /// /// - Parameters: /// - codec: The GDI+ codec info structure 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] { @@ -73,13 +73,33 @@ extension AttachableImageFormat { }.map{ $0.lowercased() } // Vestiges of MS-DOS... } + /// Get the `CLSID` value 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 + /// `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) + } + } + }.map(\.Clsid) + } + /// Get the `CLSID` value 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 /// `nil` if one could not be determined. private static func _computeCLSID(forPreferredName preferredName: String) -> CLSID? { @@ -89,16 +109,7 @@ extension AttachableImageFormat { guard S_OK == PathCchFindExtension(preferredName, wcslen(preferredName) + 1, &dot), let dot, dot[0] != 0 else { return nil } - let ext = dot + 1 - - return _allCodecs?.first { codec in - _pathExtensions(for: codec) - .contains { codecExtension in - codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in - 0 == _wcsicmp(ext, codecExtension) - } - } - }.map(\.Clsid) + return _computeCLSID(forPathExtension: dot + 1) } } @@ -133,18 +144,18 @@ extension AttachableImageFormat { /// Append the path extension preferred by GDI+ for the given `CLSID` value /// representing an image format to a suggested extension 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. 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. - if let existingCLSID = _computeCLSID(forPreferredName: preferredName), 0 != IsEqualGUID(clsid, existingCLSID) { + if let existingCLSID = _computeCLSID(forPreferredName: preferredName), clsid == existingCLSID { return preferredName } @@ -161,10 +172,10 @@ extension AttachableImageFormat { /// Get a `CLSID` value corresponding to the image format with the given MIME /// type. - /// + /// /// - Parameters: /// - mimeType: The MIME type of the image format of interest. - /// + /// /// - Returns: A `CLSID` value suitable for use with GDI+, or `nil` if none /// was found corresponding to `mimeType`. private static func _clsid(forMIMEType mimeType: String) -> CLSID? { @@ -199,7 +210,7 @@ extension AttachableImageFormat { } } - /// Initialize an instance of this type with the given `CLSID` value` and + /// Construct an instance of this type with the given `CLSID` value and /// encoding quality. /// /// - Parameters: @@ -219,5 +230,45 @@ extension AttachableImageFormat { public init(_ clsid: CLSID, encodingQuality: Float = 1.0) { self.init(kind: .systemValue(clsid), encodingQuality: encodingQuality) } + + /// Construct an instance of this type with the given path extension and + /// encoding quality. + /// + /// - Parameters: + /// - pathExtension: A path extension corresponding to the image format 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, + /// 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. + public init?(pathExtension: String, encodingQuality: Float = 1.0) { + let pathExtension = pathExtension.drop { $0 == "." } + let clsid = pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in + Self._computeCLSID(forPathExtension: pathExtension) + } + if let clsid { + self.init(clsid, encodingQuality: encodingQuality) + } else { + return nil + } + } +} + +// MARK: - + +func ==(lhs: CLSID, rhs: CLSID) -> Bool { + // Using IsEqualGUID() from the Windows SDK triggers an AST->SIL failure. Work + // around it by implementing an equivalent function ourselves. + // BUG: https://github.com/swiftlang/swift/issues/83452 + var lhs = lhs + var rhs = rhs + return 0 == memcmp(&lhs, &rhs) } #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift index efde371c3..4db1c0d85 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift @@ -15,7 +15,7 @@ private import _TestingInternals.GDIPlus internal import WinSDK /// A GDI+ image. -/// +/// /// Instances of this type represent GDI+ images (that is, instances of /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). @_spi(Experimental) @@ -26,15 +26,15 @@ internal import WinSDK private var _deleteWhenDone: Bool /// Construct an instance of this type by cloning an existing GDI+ image. - /// + /// /// - Parameters: /// - imageAddress: The address of an existing GDI+ image of type /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). - /// + /// /// This initializer makes a copy of `imageAddress` by calling its[`Clone()`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nf-gdiplusheaders-image-clone) /// function. The caller is responsible for ensuring that the resources /// backing the resulting image remain valid until it is deinitialized. - /// + /// /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the /// result is undefined. public init(unsafe imageAddress: OpaquePointer) { @@ -43,14 +43,14 @@ internal import WinSDK } /// Construct an instance of this type by borrowing an existing GDI+ image. - /// + /// /// - Parameters: /// - imageAddress: The address of an existing GDI+ image of type /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). - /// + /// /// The caller is responsible for ensuring that the resources backing the /// resulting image remain valid until it is deinitialized. - /// + /// /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the /// result is undefined. init(borrowing imageAddress: OpaquePointer) { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 1c800dc4d..0c5f2ae51 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -35,7 +35,7 @@ public struct _AttachableImageWrapper: ~Copyable where Image: AttachableA /// Whether or not to call `_cleanUpAttachment(at:)` on `pointer` when this /// instance is deinitialized. - /// + /// /// - Note: If cleanup is not performed, `pointer` is effectively being /// borrowed from the calling context. var cleanUpWhenDone: Bool diff --git a/Tests/TestingTests/AttachmentTests.swift b/Tests/TestingTests/AttachmentTests.swift index ae544d87b..21b95db52 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -735,7 +735,7 @@ extension AttachmentTests { defer { DeleteDC(dc) } - + let bitmap = try #require(CreateCompatibleBitmap(screenDC, width, height)) let oldSelectedObject = SelectObject(dc, bitmap) defer { From d2d1becb6f0b46529daa72b711e82643b42418dd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 08:25:45 -0400 Subject: [PATCH 18/25] Cleanup, _withGDIPlusImage -> _copyAttachableGDIPlusImage --- .../AttachableAsGDIPlusImage.swift | 88 +++++++++---------- .../Attachment+AttachableAsGDIPlusImage.swift | 1 - .../Attachments/GDIPlusImage.swift | 81 ----------------- .../HBITMAP+AttachableAsGDIPlusImage.swift | 15 +--- .../HICON+AttachableAsGDIPlusImage.swift | 15 +--- ...ablePointer+AttachableAsGDIPlusImage.swift | 9 +- .../Attachments/_AttachableImageWrapper.swift | 5 +- Sources/_TestingInternals/GDI+/include/GDI+.h | 5 ++ 8 files changed, 55 insertions(+), 164 deletions(-) delete mode 100644 Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index 200530b84..f09187c30 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -32,20 +32,21 @@ internal import WinSDK /// 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: ~Copyable { - /// Call a function and pass a GDI+ image representing this instance to it. +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. - /// - attachment: The attachment that is requesting an image (that is, the - /// attachment containing this instance.) - /// - body: A function to call. A copy of this instance converted to a GDI+ - /// image is passed to it. /// - /// - Returns: Whatever is returned by `body`. + /// - Returns: A pointer to a new GDI+ image representing this image. The + /// caller is responsible for deleting this image when done with it. /// - /// - Throws: Whatever is thrown by `body`, or any error that prevented the - /// creation of the buffer. + /// - 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 @@ -53,12 +54,8 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// thread when it is called. /// /// - Warning: Do not call this function directly. Instead, call - /// ``AttachableAsGDIPlusImage/withGDIPlusImage(for:_:)``. - static func _withGDIPlusImage( - at imageAddress: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage + /// ``AttachableAsGDIPlusImage/withGDIPlusImage(_:)``. + static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer /// Clean up any resources at the given address. /// @@ -66,8 +63,11 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// - imageAddress: The address of the instance of this type. /// /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) at `imageAddress`. This function is invoked - /// automatically by `_AttachableImageWrapper` when it is deinitialized. + /// 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:)`. /// /// - Warning: Do not call this function directly. static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) @@ -92,18 +92,16 @@ public protocol _AttachableByAddressAsGDIPlusImage: ~Copyable { /// first convert it to an instance of one of the types above. @_spi(Experimental) public protocol AttachableAsGDIPlusImage { - /// Call a function and pass a GDI+ image representing this instance to it. + /// Create a GDI+ image representing this instance. /// - /// - Parameters: - /// - attachment: The attachment that is requesting an image (that is, the - /// attachment containing this instance.) - /// - body: A function to call. A copy of this instance converted to a GDI+ - /// image is passed to it. + /// - Returns: A pointer to a new GDI+ image representing this image. The + /// caller is responsible for deleting this image when done with it. /// - /// - Returns: Whatever is returned by `body`. + /// - Throws: Any error that prevented the creation of the GDI+ image. /// - /// - Throws: Whatever is thrown by `body`, or any error that prevented the - /// creation of the buffer. + /// - 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 @@ -111,20 +109,17 @@ public protocol AttachableAsGDIPlusImage { /// thread when it is called. /// /// - Warning: Do not call this function directly. Instead, call - /// ``UnsafeMutablePointer/withGDIPlusImage(for:_:)``. - func _withGDIPlusImage( - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage + /// ``AttachableAsGDIPlusImage/withGDIPlusImage(_:)``. + func _copyAttachableGDIPlusImage() throws -> OpaquePointer - /// Clean up any resources at the given address. - /// - /// - Parameters: - /// - imageAddress: The address of the instance of this type. + /// Clean up any resources associated with this instance. /// /// The implementation of this function cleans up any resources (such as - /// handles or COM objects) at `imageAddress`. This function is invoked - /// automatically by `_AttachableImageWrapper` when it is deinitialized. + /// 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()`. /// /// - Warning: Do not call this function directly. func _cleanUpAttachment() @@ -144,19 +139,18 @@ extension AttachableAsGDIPlusImage { /// - Throws: Whatever is thrown by `body`, or any error that prevented the /// creation of the buffer. /// + /// - Note: The argument passed to `body` is of C++ type `Gdiplus::Image *`. + /// That type cannot be directly represented in Swift. + /// /// This function is a convenience wrapper around `_withGDIPlusImage()` that /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. - func withGDIPlusImage( - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage { - // Stuff the attachment into a pointer so we can reference it from within - // the closure we pass to `withGDIPlus(_:)`. (The compiler currently can't - // reason about the lifetime of a borrowed value passed into a closure.) - try withUnsafePointer(to: attachment) { attachment in - try withGDIPlus { - try _withGDIPlusImage(for: attachment.pointee, body) + func withGDIPlusImage(_ body: (borrowing OpaquePointer) throws -> R) throws -> R { + try withGDIPlus { + let image = try _copyGDIPlusImage() + defer { + swt_GdiplusImageDelete(image) } + return try body(image) } } } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift index e51a11275..54b24d435 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/Attachment+AttachableAsGDIPlusImage.swift @@ -12,7 +12,6 @@ @_spi(Experimental) public import Testing @_spi(Experimental) -@available(_uttypesAPI, *) extension Attachment where AttachableValue: ~Copyable { /// Initialize an instance of this type that encloses the given image. /// diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift deleted file mode 100644 index 4db1c0d85..000000000 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDIPlusImage.swift +++ /dev/null @@ -1,81 +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 GDI+ image. -/// -/// Instances of this type represent GDI+ images (that is, instances of -/// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). -@_spi(Experimental) -@unsafe public struct GDIPlusImage: ~Copyable { - /// The address of the C++ `Gdiplus::Image` instance. - var imageAddress: OpaquePointer - - private var _deleteWhenDone: Bool - - /// Construct an instance of this type by cloning an existing GDI+ image. - /// - /// - Parameters: - /// - imageAddress: The address of an existing GDI+ image of type - /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). - /// - /// This initializer makes a copy of `imageAddress` by calling its[`Clone()`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nf-gdiplusheaders-image-clone) - /// function. The caller is responsible for ensuring that the resources - /// backing the resulting image remain valid until it is deinitialized. - /// - /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the - /// result is undefined. - public init(unsafe imageAddress: OpaquePointer) { - self.imageAddress = swt_GdiplusImageClone(imageAddress) - self._deleteWhenDone = true - } - - /// Construct an instance of this type by borrowing an existing GDI+ image. - /// - /// - Parameters: - /// - imageAddress: The address of an existing GDI+ image of type - /// [`Gdiplus.Image`](https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nl-gdiplusheaders-image)). - /// - /// The caller is responsible for ensuring that the resources backing the - /// resulting image remain valid until it is deinitialized. - /// - /// - Important: If `imageAddress` is not a pointer to a GDI+ image, the - /// result is undefined. - init(borrowing imageAddress: OpaquePointer) { - self.imageAddress = imageAddress - self._deleteWhenDone = false - } - - deinit { - if _deleteWhenDone { - swt_GdiplusImageDelete(imageAddress) - } - } -} - -extension GDIPlusImage: _AttachableByAddressAsGDIPlusImage { - public static func _withGDIPlusImage( - at imageAddress: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage { - try body(imageAddress) - } - - public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { - swt_GdiplusImageDelete(imageAddress.pointee.imageAddress) - } -} -#endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 5b9cdd045..2d50dc241 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -16,19 +16,8 @@ public import WinSDK @_spi(Experimental) extension HBITMAP__: _AttachableByAddressAsGDIPlusImage { - public static func _withGDIPlusImage( - at imageAddress: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage { - let image = swt_GdiplusImageFromHBITMAP(imageAddress, nil) - defer { - swt_GdiplusImageDelete(image) - } - return try withExtendedLifetime(self) { - var image: GDIPlusImage = GDIPlusImage(borrowing: image) - return try body(&image) - } + public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { + swt_GdiplusImageFromHBITMAP(imageAddress, nil) } public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index 4064bd0fa..17a6dbd59 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -16,19 +16,8 @@ public import WinSDK @_spi(Experimental) extension HICON__: _AttachableByAddressAsGDIPlusImage { - public static func _withGDIPlusImage( - at imageAddress: UnsafeMutablePointer, - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage { - let image = swt_GdiplusImageFromHICON(imageAddress) - defer { - swt_GdiplusImageDelete(image) - } - return try withExtendedLifetime(self) { - var image = GDIPlusImage(borrowing: image) - return try body(&image) - } + public static func _copyAttachableGDIPlusImage(at imageAddress: UnsafeMutablePointer) throws -> OpaquePointer { + swt_GdiplusImageFromHICON(imageAddress) } public static func _cleanUpAttachment(at imageAddress: UnsafeMutablePointer) { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift index 5b52ff6ab..941e08f7d 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -12,12 +12,9 @@ @_spi(Experimental) public import Testing @_spi(Experimental) -extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage & ~Copyable { - public func _withGDIPlusImage( - for attachment: borrowing Attachment<_AttachableImageWrapper>, - _ body: (borrowing UnsafeMutablePointer) throws -> R - ) throws -> R where A: AttachableAsGDIPlusImage { - try Pointee._withGDIPlusImage(at: self, for: attachment, body) +extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { + public func _copyAttachableGDIPlusImage() throws -> OpaquePointer { + try Pointee._copyAttachableGDIPlusImage(at: self) } public func _cleanUpAttachment() { diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index 0c5f2ae51..ef2c300fe 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -58,7 +58,6 @@ extension _AttachableImageWrapper: Sendable {} // MARK: - -@available(_uttypesAPI, *) extension _AttachableImageWrapper: AttachableWrapper { public var wrappedValue: Image { image @@ -96,8 +95,8 @@ extension _AttachableImageWrapper: AttachableWrapper { encoderParams.Parameter.Value = encodingQuality.baseAddress // Save the image into the stream. - try image.withGDIPlusImage(for: attachment) { image in - let rSave = swt_GdiplusImageSave(image.pointee.imageAddress, stream, &clsid, &encoderParams) + try image.withGDIPlusImage { image in + let rSave = swt_GdiplusImageSave(image, stream, &clsid, &encoderParams) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) } diff --git a/Sources/_TestingInternals/GDI+/include/GDI+.h b/Sources/_TestingInternals/GDI+/include/GDI+.h index 4b510f1b2..ff3020b66 100644 --- a/Sources/_TestingInternals/GDI+/include/GDI+.h +++ b/Sources/_TestingInternals/GDI+/include/GDI+.h @@ -11,6 +11,11 @@ #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" From a8f3c68679638546ada467e3bc80f2553dc5046a Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 08:35:30 -0400 Subject: [PATCH 19/25] Fix typos --- .../Attachments/AttachableAsGDIPlusImage.swift | 2 +- .../Attachments/AttachableImageFormat+CLSID.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index f09187c30..ed81ce3c1 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -146,7 +146,7 @@ extension AttachableAsGDIPlusImage { /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. func withGDIPlusImage(_ body: (borrowing OpaquePointer) throws -> R) throws -> R { try withGDIPlus { - let image = try _copyGDIPlusImage() + let image = try _copyAttachableGDIPlusImage() defer { swt_GdiplusImageDelete(image) } diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 09a2d6dc2..b2439d72e 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -82,7 +82,7 @@ extension AttachableImageFormat { /// /// - Returns: An instance of `CLSID` referring to a concrete image type, or /// `nil` if one could not be determined. - private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { + private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { _allCodecs?.first { codec in _pathExtensions(for: codec) .contains { codecExtension in @@ -269,6 +269,6 @@ func ==(lhs: CLSID, rhs: CLSID) -> Bool { // BUG: https://github.com/swiftlang/swift/issues/83452 var lhs = lhs var rhs = rhs - return 0 == memcmp(&lhs, &rhs) + return 0 == memcmp(&lhs, &rhs, MemoryLayout.size) } #endif From fb4ce8e9325b048a24eb4b027758186ff1501a8d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 11:52:33 -0400 Subject: [PATCH 20/25] Simplify the GDI+ image logic by having the protocol just create them and letting the library call delete --- .../AttachableAsGDIPlusImage.swift | 44 ++++--------------- .../Attachments/_AttachableImageWrapper.swift | 39 +++++++++------- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift index ed81ce3c1..daaf88df1 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableAsGDIPlusImage.swift @@ -53,8 +53,8 @@ public protocol _AttachableByAddressAsGDIPlusImage { /// can therefore assume that GDI+ is correctly configured on the current /// thread when it is called. /// - /// - Warning: Do not call this function directly. Instead, call - /// ``AttachableAsGDIPlusImage/withGDIPlusImage(_:)``. + /// 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. @@ -69,7 +69,8 @@ public protocol _AttachableByAddressAsGDIPlusImage { /// This function is not responsible for deleting the image returned from /// `_copyAttachableGDIPlusImage(at:)`. /// - /// - Warning: Do not call this function directly. + /// 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) } @@ -108,8 +109,8 @@ public protocol AttachableAsGDIPlusImage { /// can therefore assume that GDI+ is correctly configured on the current /// thread when it is called. /// - /// - Warning: Do not call this function directly. Instead, call - /// ``AttachableAsGDIPlusImage/withGDIPlusImage(_:)``. + /// 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. @@ -121,37 +122,8 @@ public protocol AttachableAsGDIPlusImage { /// This function is not responsible for deleting the image returned from /// `_copyAttachableGDIPlusImage()`. /// - /// - Warning: Do not call this function directly. + /// This function is not part of the public interface of the testing library. + /// It may be removed in a future update. func _cleanUpAttachment() } - -extension AttachableAsGDIPlusImage { - /// Call a function and pass a GDI+ image representing this instance to it. - /// - /// - Parameters: - /// - attachment: The attachment that is requesting an image (that is, the - /// attachment containing this instance.) - /// - body: A function to call. A copy of this instance converted to a GDI+ - /// image is passed to it. - /// - /// - Returns: Whatever is returned by `body`. - /// - /// - Throws: Whatever is thrown by `body`, or any error that prevented the - /// creation of the buffer. - /// - /// - Note: The argument passed to `body` is of C++ type `Gdiplus::Image *`. - /// That type cannot be directly represented in Swift. - /// - /// This function is a convenience wrapper around `_withGDIPlusImage()` that - /// calls `GdiplusStartup()` and `GdiplusShutdown()` at the appropriate times. - func withGDIPlusImage(_ body: (borrowing OpaquePointer) throws -> R) throws -> R { - try withGDIPlus { - let image = try _copyAttachableGDIPlusImage() - defer { - swt_GdiplusImageDelete(image) - } - return try body(image) - } - } -} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index ef2c300fe..a9478bb69 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -78,24 +78,29 @@ extension _AttachableImageWrapper: AttachableWrapper { } } - // Get the CLSID of the image encoder corresponding to the specified image - // format. - let imageFormat = self.imageFormat ?? .png - guard var clsid = imageFormat.clsid else { - throw GDIPlusError.clsidNotFound - } + try withGDIPlus { + // Get a GDI+ image from the attachment. + let image = try image._copyAttachableGDIPlusImage() + defer { + swt_GdiplusImageDelete(image) + } + + // Get the CLSID of the image encoder corresponding to the specified image + // format. + guard var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) else { + throw GDIPlusError.clsidNotFound + } + + 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 - var encodingQuality = LONG(imageFormat.encodingQuality * 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. - try image.withGDIPlusImage { image in + // Save the image into the stream. let rSave = swt_GdiplusImageSave(image, stream, &clsid, &encoderParams) guard rSave == Gdiplus.Ok else { throw GDIPlusError.status(rSave) From a9840b6d054b856209eecbe12d9b38c55820d3f3 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 12:16:24 -0400 Subject: [PATCH 21/25] Remove some redundant logic around CLSIDs --- .../AttachableImageFormat+CLSID.swift | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index b2439d72e..55f2457b0 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -26,7 +26,7 @@ extension AttachableImageFormat { var byteCount = UINT(0) let rGetSize = Gdiplus.GetImageEncodersSize(&codecCount, &byteCount) guard rGetSize == Gdiplus.Ok else { - return nil + throw GDIPlusError.status(rGetSize) } // Allocate a buffer of sufficient byte size, then bind the leading bytes @@ -44,7 +44,7 @@ extension AttachableImageFormat { let rGetEncoders = Gdiplus.GetImageEncoders(codecCount, byteCount, codecBuffer.baseAddress!) guard rGetEncoders == Gdiplus.Ok else { result.deallocate() - return nil + throw GDIPlusError.status(rGetEncoders) } return .init(codecBuffer) } @@ -93,6 +93,20 @@ extension AttachableImageFormat { }.map(\.Clsid) } + /// Get the `CLSID` value 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 + /// `nil` if one could not be determined. + private static func _computeCLSID(forPathExtension pathExtension: String) -> CLSID? { + pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in + _computeCLSID(forPathExtension: pathExtension) + } + } + /// Get the `CLSID` value corresponding to the same image format as the path /// extension on the given attachment filename. /// @@ -125,7 +139,7 @@ extension AttachableImageFormat { /// `nil` if one could not be determined. /// /// This function is not part of the public interface of the testing library. - static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID? { + static func computeCLSID(for imageFormat: Self?, withPreferredName preferredName: String) -> CLSID { if let clsid = imageFormat?.clsid { return clsid } @@ -170,43 +184,29 @@ extension AttachableImageFormat { return "\(preferredName).\(ext)" } - /// Get a `CLSID` value corresponding to the image format with the given MIME - /// type. - /// - /// - Parameters: - /// - mimeType: The MIME type of the image format of interest. - /// - /// - Returns: A `CLSID` value suitable for use with GDI+, or `nil` if none - /// was found corresponding to `mimeType`. - private static func _clsid(forMIMEType mimeType: String) -> CLSID? { - mimeType.withCString(encodedAs: UTF16.self) { mimeType in - _allCodecs?.first { 0 == wcscmp($0.MimeType, mimeType) }?.Clsid - } - } - /// 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 = _clsid(forMIMEType: "image/png") + 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 = _clsid(forMIMEType: "image/jpeg") + private static let _jpegCLSID = _computeCLSID(forPathExtension: "jpg")! /// The `CLSID` value corresponding to this image format. - public var clsid: CLSID? { + public var clsid: CLSID { switch kind { case .png: Self._pngCLSID case .jpeg: Self._jpegCLSID case let .systemValue(clsid): - clsid as? CLSID + clsid as! CLSID } } @@ -250,9 +250,7 @@ extension AttachableImageFormat { /// function. public init?(pathExtension: String, encodingQuality: Float = 1.0) { let pathExtension = pathExtension.drop { $0 == "." } - let clsid = pathExtension.withCString(encodedAs: UTF16.self) { pathExtension in - Self._computeCLSID(forPathExtension: pathExtension) - } + let clsid = Self._computeCLSID(forPathExtension: String(pathExtension)) if let clsid { self.init(clsid, encodingQuality: encodingQuality) } else { From 0a079db9475e9cc7a6b04c2c5b6e21b09bd5da34 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 12:32:09 -0400 Subject: [PATCH 22/25] Get rid of some redundant code --- .../AttachableImageFormat+CLSID.swift | 25 +++++++------------ .../_Testing_WinSDK/Attachments/GDI+.swift | 17 ++++++++++--- .../Attachments/_AttachableImageWrapper.swift | 8 ++---- Tests/TestingTests/AttachmentTests.swift | 4 +-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index 55f2457b0..db45dc233 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -19,8 +19,8 @@ extension AttachableImageFormat { /// /// If the testing library was unable to determine the set of image formats, /// the value of this property is `nil`. - private static nonisolated(unsafe) let _allCodecs: UnsafeBufferPointer? = { - try? withGDIPlus { + private static nonisolated(unsafe) let _allCodecs: [Gdiplus.ImageCodecInfo] = { + let result = try? withGDIPlus { // Find out the size of the buffer needed. var codecCount = UINT(0) var byteCount = UINT(0) @@ -36,6 +36,9 @@ extension AttachableImageFormat { byteCount: Int(byteCount), alignment: MemoryLayout.alignment ) + defer { + result.deallocate() + } let codecBuffer = result .prefix(MemoryLayout.stride * Int(codecCount)) .bindMemory(to: Gdiplus.ImageCodecInfo.self) @@ -46,8 +49,9 @@ extension AttachableImageFormat { result.deallocate() throw GDIPlusError.status(rGetEncoders) } - return .init(codecBuffer) + return Array(codecBuffer) } + return result ?? [] }() /// Get the set of path extensions corresponding to the image format @@ -83,7 +87,7 @@ extension AttachableImageFormat { /// - Returns: An instance of `CLSID` referring to a concrete image type, or /// `nil` if one could not be determined. private static func _computeCLSID(forPathExtension pathExtension: UnsafePointer) -> CLSID? { - _allCodecs?.first { codec in + _allCodecs.first { codec in _pathExtensions(for: codec) .contains { codecExtension in codecExtension.withCString(encodedAs: UTF16.self) { codecExtension in @@ -173,7 +177,7 @@ extension AttachableImageFormat { return preferredName } - let ext = _allCodecs? + let ext = _allCodecs .first { $0.Clsid == clsid } .flatMap { _pathExtensions(for: $0).first } guard let ext else { @@ -258,15 +262,4 @@ extension AttachableImageFormat { } } } - -// MARK: - - -func ==(lhs: CLSID, rhs: CLSID) -> Bool { - // Using IsEqualGUID() from the Windows SDK triggers an AST->SIL failure. Work - // around it by implementing an equivalent function ourselves. - // BUG: https://github.com/swiftlang/swift/issues/83452 - var lhs = lhs - var rhs = rhs - return 0 == memcmp(&lhs, &rhs, MemoryLayout.size) -} #endif diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift index 9188705d3..9535cedfd 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/GDI+.swift @@ -14,11 +14,16 @@ 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) - case clsidNotFound } extension GDIPlusError: CustomStringConvertible { @@ -30,14 +35,20 @@ extension GDIPlusError: CustomStringConvertible { "Could not create an in-memory stream (HRESULT \(result))." case let .globalFromStreamFailed(result): "Could not access the buffer containing the encoded image (HRESULT \(result))." - case .clsidNotFound: - "Could not find an appropriate CSLID value for the specified image format." } } } // 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. diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift index a9478bb69..9f23cb140 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/_AttachableImageWrapper.swift @@ -87,9 +87,7 @@ extension _AttachableImageWrapper: AttachableWrapper { // Get the CLSID of the image encoder corresponding to the specified image // format. - guard var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) else { - throw GDIPlusError.clsidNotFound - } + var clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: attachment.preferredName) var encodingQuality = LONG((imageFormat?.encodingQuality ?? 1.0) * 100.0) try withUnsafeMutableBytes(of: &encodingQuality) { encodingQuality in @@ -127,9 +125,7 @@ extension _AttachableImageWrapper: AttachableWrapper { } public borrowing func preferredName(for attachment: borrowing Attachment, basedOn suggestedName: String) -> String { - guard let clsid = AttachableImageFormat.computeCLSID(for: imageFormat, withPreferredName: suggestedName) else { - return suggestedName - } + 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 21b95db52..d1556ff87 100644 --- a/Tests/TestingTests/AttachmentTests.swift +++ b/Tests/TestingTests/AttachmentTests.swift @@ -768,11 +768,11 @@ extension AttachmentTests { } @MainActor @Test func pathExtensionAndCLSID() throws { - let pngCLSID = try #require(AttachableImageFormat.png.clsid) + let pngCLSID = AttachableImageFormat.png.clsid let pngFilename = AttachableImageFormat.appendPathExtension(for: pngCLSID, to: "example") #expect(pngFilename == "example.png") - let jpegCLSID = try #require(AttachableImageFormat.jpeg.clsid) + let jpegCLSID = AttachableImageFormat.jpeg.clsid let jpegFilename = AttachableImageFormat.appendPathExtension(for: jpegCLSID, to: "example") #expect(jpegFilename == "example.jpg") From f2dc01510acf8b4a7ac8205dae16581db0496abf Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 12:53:43 -0400 Subject: [PATCH 23/25] Microsoft defines DWORD as unsigned long, not unsigned int (despite the documentation) --- Sources/Testing/Support/CError.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Support/CError.swift b/Sources/Testing/Support/CError.swift index 572775527..b392191d1 100644 --- a/Sources/Testing/Support/CError.swift +++ b/Sources/Testing/Support/CError.swift @@ -28,9 +28,9 @@ struct CError: Error, RawRepresentable { /// /// This type is not part of the public interface of the testing library. package struct Win32Error: Error, RawRepresentable { - package var rawValue: CUnsignedInt + package var rawValue: CUnsignedLong - package init(rawValue: CUnsignedInt) { + package init(rawValue: CUnsignedLong) { self.rawValue = rawValue } } From 241f5bac81de701bd7178485b252dafafeae3cc2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 13:03:10 -0400 Subject: [PATCH 24/25] Suppress some warnings --- .../Attachments/HBITMAP+AttachableAsGDIPlusImage.swift | 2 +- .../Attachments/HICON+AttachableAsGDIPlusImage.swift | 2 +- .../UnsafeMutablePointer+AttachableAsGDIPlusImage.swift | 2 +- Sources/Testing/Attachments/AttachableImageFormat.swift | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift index 2d50dc241..466a992ec 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HBITMAP+AttachableAsGDIPlusImage.swift @@ -9,7 +9,7 @@ // #if os(Windows) -@_spi(Experimental) public import Testing +import Testing private import _TestingInternals.GDIPlus public import WinSDK diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift index 17a6dbd59..0269bee56 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/HICON+AttachableAsGDIPlusImage.swift @@ -9,7 +9,7 @@ // #if os(Windows) -@_spi(Experimental) public import Testing +import Testing private import _TestingInternals.GDIPlus public import WinSDK diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift index 941e08f7d..5a50ef1e7 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/UnsafeMutablePointer+AttachableAsGDIPlusImage.swift @@ -9,7 +9,7 @@ // #if os(Windows) -@_spi(Experimental) public import Testing +import Testing @_spi(Experimental) extension UnsafeMutablePointer: AttachableAsGDIPlusImage where Pointee: _AttachableByAddressAsGDIPlusImage { diff --git a/Sources/Testing/Attachments/AttachableImageFormat.swift b/Sources/Testing/Attachments/AttachableImageFormat.swift index afb74c80e..bf5df4f7f 100644 --- a/Sources/Testing/Attachments/AttachableImageFormat.swift +++ b/Sources/Testing/Attachments/AttachableImageFormat.swift @@ -42,7 +42,8 @@ public struct AttachableImageFormat: Sendable { /// use. The platform-specific cross-import overlay or package is /// responsible for exposing appropriate interfaces for this case. /// - /// On Apple platforms, `value` should be an instance of `UTType`. + /// On Apple platforms, `value` should be an instance of `UTType`. On + /// Windows, it should be an instance of `CLSID`. case systemValue(_ value: any Sendable) } From 125989b95d8038158cfc2297fd35de10ee911012 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 31 Jul 2025 14:53:04 -0400 Subject: [PATCH 25/25] Oh right, that buffer is used --- .../Attachments/AttachableImageFormat+CLSID.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift index db45dc233..8985b2aeb 100644 --- a/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift +++ b/Sources/Overlays/_Testing_WinSDK/Attachments/AttachableImageFormat+CLSID.swift @@ -19,7 +19,11 @@ extension AttachableImageFormat { /// /// If the testing library was unable to determine the set of image formats, /// the value of this property is `nil`. - private static nonisolated(unsafe) let _allCodecs: [Gdiplus.ImageCodecInfo] = { + /// + /// - 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) @@ -36,9 +40,6 @@ extension AttachableImageFormat { byteCount: Int(byteCount), alignment: MemoryLayout.alignment ) - defer { - result.deallocate() - } let codecBuffer = result .prefix(MemoryLayout.stride * Int(codecCount)) .bindMemory(to: Gdiplus.ImageCodecInfo.self) @@ -49,9 +50,9 @@ extension AttachableImageFormat { result.deallocate() throw GDIPlusError.status(rGetEncoders) } - return Array(codecBuffer) + return UnsafeBufferPointer(codecBuffer) } - return result ?? [] + return result ?? UnsafeBufferPointer(start: nil, count: 0) }() /// Get the set of path extensions corresponding to the image format