Swift Digest
ST-0015 | Swift Evolution

Swift Testing での画像添付(Windows)

Image attachments in Swift Testing (Windows)

Proposal
ST-0015
Authors
Jonathan Grynspan
Review Manager
Stuart Montgomery
Status
Implemented (Swift 6.3)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

ST-0014 では Apple プラットフォーム向けに、CGImage / CIImage / NSImage / UIImage といった標準の画像型をそのまま Attachment.record(...) に渡してテストに添付できる仕組みが導入されました。UI のレンダリング結果や CI 上で発生した描画不具合のスクリーンショットなどを、エンコードを自分で書かずに添付できるのは便利な機能です。

しかし Swift Testing はクロスプラットフォームのテスティングライブラリであり、画像添付の仕組みも Apple プラットフォームだけに留めておくべきではありません。Windows 上のテストでも、HBITMAPHICON、Windows Imaging Component(WIC)のビットマップなどを直接添付できるようにすることが望まれていました。

Windows の画像 API には、初期からある Graphics Device Interface(GDI)の HBITMAP / HICON と、より新しい WIC(COM ベース)の IWICBitmapSource などがあります。HBITMAP / HICON は参照カウントを持たないハンドル(pointer-to-pointer)で、Swift には UnsafeMutablePointer のエイリアスとして取り込まれます。WIC の COM クラスは C++ の IUnknown を継承するクラスですが、現状の Swift の C/C++ インポータは COM クラスを参照カウント付きの Swift クラスとしては扱わず、こちらも UnsafeMutablePointer<T> として取り込まれます。

つまり Windows 側の画像型はいずれも UnsafeMutablePointer の特殊化として現れるのですが、UnsafeMutablePointer のすべての特殊化を画像として扱うわけにはいきません。画像として添付できる型を限定したうえで、それらを統一的に扱える仕組みが必要でした。

02 どのように解決されるのか

Windows 上でも、テスト中に得た画像をそのまま Attachment.record(...) に渡して添付できるようにします。エンコードには Windows 標準の Windows Imaging Component(WIC)を利用します。Apple プラットフォームの ST-0014 と同じ構造を採り、画像型を表すプロトコルと Attachment のオーバーロード、そして画像形式を表す AttachableImageFormat の Windows 向けの拡張という 3 点で構成されます。

AttachableAsIWICBitmapSource プロトコル

Windows 上で画像として添付できる型を表すプロトコルとして AttachableAsIWICBitmapSource が追加されます。Apple プラットフォームの AttachableAsCGImage に対応するプロトコルで、要求は WIC のビットマップソース(IWICBitmapSource)を生成することです。

public protocol AttachableAsIWICBitmapSource: SendableMetatype {
  /// Create a WIC bitmap source representing an instance of this type.
  ///
  /// - Returns: A pointer to a new WIC bitmap source representing this image.
  ///   The caller is responsible for releasing this image when done with it.
  func copyAttachableIWICBitmapSource() throws -> UnsafeMutablePointer<IWICBitmapSource>
}

このプロトコルには、Windows 標準の次の画像型があらかじめ適合しています。

  • HBITMAP(GDI のビットマップハンドル)
  • HICON(GDI のアイコンハンドル)
  • IWICBitmapSource(および WIC が定義するそのサブクラス)

Swift では同じ型に対して、異なる制約で同じプロトコルへの適合を複数回与えることはできません。UnsafeMutablePointer を上記の Pointee ごとに条件付きで適合させるため、補助プロトコル _AttachableByAddressAsIWICBitmapSource を経由する形で適合が定義されています。

public protocol _AttachableByAddressAsIWICBitmapSource {}

extension HBITMAP.Pointee: _AttachableByAddressAsIWICBitmapSource {}
extension HICON.Pointee: _AttachableByAddressAsIWICBitmapSource {}
extension IWICBitmapSource: _AttachableByAddressAsIWICBitmapSource {}

extension UnsafeMutablePointer: AttachableAsIWICBitmapSource
  where Pointee: _AttachableByAddressAsIWICBitmapSource {}

_AttachableByAddressAsIWICBitmapSource は API としては public ですが実装の詳細であり、本 Proposal の範囲外として扱われます。Swift に COM 相互運用が入れば、この補助プロトコル自体が不要になる見込みです(「今後の見通し」を参照)。

テスト作者が自分で AttachableAsIWICBitmapSource への適合を追加する必要は通常ありません。別形式の画像を扱いたい場合は、上記の標準型のいずれかに変換してから渡します。なお、適合する型は将来追加される可能性があり、その判断は Testing Workgroup が行います。

画像を添付する

Attachment に、AttachableAsIWICBitmapSource に適合する値を直接渡せる初期化子と record(...) のオーバーロードが追加されます。

extension Attachment {
  public init<T>(
    _ image: T,
    named preferredName: String? = nil,
    as imageFormat: AttachableImageFormat? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
  ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper<T>

  public static func record<T>(
    _ image: T,
    named preferredName: String? = nil,
    as imageFormat: AttachableImageFormat? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
  ) where T: AttachableAsIWICBitmapSource, AttachableValue == _AttachableImageWrapper<T>
}

_AttachableImageWrapper は ST-0014 で導入された内部のラッパー型で、Windows でも同じ型が利用されます。Apple プラットフォームとの違いは、関連型 ImageAttachableAsCGImage ではなく AttachableAsIWICBitmapSource で制約されている点だけです。

imageFormat の挙動も ST-0014 と同様で、nil を渡したときはまず preferredName の拡張子から形式を推測し、それでも決まらなければライブラリが適切な形式を選びます。

Windows 向けの AttachableImageFormat

画像形式を抽象化する AttachableImageFormat 型自体は ST-0014 で導入済みですが、Windows は Uniform Type Identifiers を持たないため、UTType を受け取る Apple 用の API は Windows では使えません。代わりに、WIC のエンコーダ COM クラスを識別する CLSID を直接扱う API が用意されます。

extension AttachableImageFormat {
  /// The `CLSID` value of the Windows Imaging Component (WIC) encoder class
  /// that corresponds to this image format.
  public var encoderCLSID: CLSID { get }

  public init(encoderCLSID: CLSID, encodingQuality: Float = 1.0)
}

たとえば .pngencoderCLSIDCLSID_WICPngEncoder と等しくなります。encodingQuality0.0(最低品質)から 1.0(最高品質)までで、可変品質エンコーディングをサポートしないエンコーダでは無視されます。encoderCLSID が WIC のエンコーダクラスでない場合の挙動は未定義です。

加えて、拡張子から対応するエンコーダの CLSID を解決するための初期化子も追加されます。

extension AttachableImageFormat {
  public init?(pathExtension: String, encodingQuality: Float = 1.0)
}

pathExtension が認識できる画像形式に対応していない場合、この初期化子は nil を返します。Windows では「対応する IWICBitmapEncoder のサブクラスが WIC に登録されていること」が条件です。一貫性のため、この init(pathExtension:encodingQuality:) は Apple プラットフォームにも提供されます(Apple では pathExtension から得られるコンテンツタイプが UTType.image に適合していることが条件です)。これが本 Proposal で唯一、Windows 以外のプラットフォームに影響する変更です。

利用例

WIC を介して、HICON を PNG として添付する例を示します。

import Testing
import WinSDK

@MainActor @Test func `attaching an icon`() throws {
  let hIcon: HICON = ...
  defer {
    DestroyIcon(hIcon)
  }
  Attachment.record(hIcon, named: "my icon", as: .png)
  // OR: Attachment.record(hIcon, named: "my icon.png")
}

as: .png を明示する代わりに、preferredName の拡張子(my icon.png)から PNG を選ばせることもできます。

03 今後の見通し

将来の発展として、以下のようなアイデアが挙げられています。

  • COM クラスを Swift の foreign reference counted なクラスとして取り込めるようにする C++ 相互運用機能の追加。これが実現すると IWICBitmapSource のような COM クラスが UnsafeMutablePointer ではなく Swift のクラスとして直接扱えるようになり、本 Proposal で導入した補助プロトコル _AttachableByAddressAsIWICBitmapSource は不要になります。AttachableAsIWICBitmapSource の要求も、メソッド copyAttachableIWICBitmapSource() から var attachableIWICBitmapSource: IWICBitmapSource { get throws } のようなプロパティに変わると見込まれています。Swift Testing にとっては破壊的な変更となりますが、Swift から COM を扱いやすくする大きな改善になります。Swift チームは COM 相互運用を swiftlang/swift#84056 で追跡しています。
  • マネージドな(.NET / C#)画像型への対応。Swift と .NET / C# の相互運用機能が前提となるため、本 Proposal の範囲外です。
  • WinRT の画像型への対応。WinRT は COM の薄いラッパーであり、C++ や .NET 経由でしか実用的に扱えないため、こちらも COM 相互運用が実現してから検討する余地があるとされています。
  • Windows 以外のプラットフォーム(Linux、Android など)への画像添付対応。詳細は ST-0014 の「今後の見通し」で議論されている方向性に従います。

これらはいずれも将来の構想であり、実現を約束するものではありません。