-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a CoreGraphics cross-import overlay with support for attaching `C…
…GImage`s. (#827) This PR adds a new cross-import overlay target with Apple's Core Graphics framework that allows attaching a `CGImage` as an attachment in an arbitrary image format (PNG, JPEG, etc.) Because `CGImage` is imported into Swift as a non-final class, it cannot conform directly to `Attachable`, so an `AttachableContainer` type acts as a proxy. This type is not meant to be used directly, so its name is underscored. Initializers on `Attachment` are provided so that this abstraction is almost entirely transparent to test authors. A new protocol, `AttachableAsCGImage`, is introduced that abstracts away the relationship between the attached image and Core Graphics; in the future, I intend to make additional image types like `NSImage` and `UIImage` conform to this protocol too. Example usage: ```swift let sparklyDiamonds: CGImage = ... let attachment = Attachment(image, named: "sparkly-diamonds", as: .tiff, encodingQuality: 0.75) ... attachment.attach() ``` The code in this PR is, by definition, specific to Apple's platforms. In the future, I'd be interested in adding Windows/Linux equivalents (`HBITMAP`? Whatever Gnome/KDE/Qt use?) but that's beyond the scope of this PR. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
- Loading branch information
Showing
8 changed files
with
568 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
Sources/Overlays/_Testing_CoreGraphics/Attachments/AttachableAsCGImage.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// | ||
// 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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics) | ||
public import CoreGraphics | ||
private import ImageIO | ||
|
||
/// 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: | ||
/// | ||
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) | ||
/// | ||
/// 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 AttachableAsCGImage { | ||
/// An instance of `CGImage` representing this image. | ||
/// | ||
/// - Throws: Any error that prevents the creation of an image. | ||
var attachableCGImage: CGImage { get throws } | ||
|
||
/// The orientation of the image. | ||
/// | ||
/// The value of this property is the raw value of an instance of | ||
/// `CGImagePropertyOrientation`. The default value of this property is | ||
/// `.up`. | ||
/// | ||
/// This property is not part of the public interface of the testing | ||
/// library. It may be removed in a future update. | ||
var _attachmentOrientation: UInt32 { get } | ||
|
||
/// The scale factor of the image. | ||
/// | ||
/// The value of this property is typically greater than `1.0` when an image | ||
/// originates from a Retina Display screenshot or similar. The default value | ||
/// of this property is `1.0`. | ||
/// | ||
/// This property is not part of the public interface of the testing | ||
/// library. It may be removed in a future update. | ||
var _attachmentScaleFactor: CGFloat { get } | ||
|
||
/// Make a copy of this instance to pass to an attachment. | ||
/// | ||
/// - Returns: A copy of `self`, or `self` if no copy is needed. | ||
/// | ||
/// Several system image types do not conform to `Sendable`; use this | ||
/// function to make copies of such images that will not be shared outside | ||
/// of an attachment and so can be generally safely stored. | ||
/// | ||
/// The default implementation of this function when `Self` conforms to | ||
/// `Sendable` simply returns `self`. | ||
/// | ||
/// This function is not part of the public interface of the testing library. | ||
/// It may be removed in a future update. | ||
func _makeCopyForAttachment() -> Self | ||
} | ||
|
||
extension AttachableAsCGImage { | ||
public var _attachmentOrientation: UInt32 { | ||
CGImagePropertyOrientation.up.rawValue | ||
} | ||
|
||
public var _attachmentScaleFactor: CGFloat { | ||
1.0 | ||
} | ||
} | ||
|
||
extension AttachableAsCGImage where Self: Sendable { | ||
public func _makeCopyForAttachment() -> Self { | ||
self | ||
} | ||
} | ||
#endif |
147 changes: 147 additions & 0 deletions
147
Sources/Overlays/_Testing_CoreGraphics/Attachments/Attachment+AttachableAsCGImage.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// | ||
// 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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics) | ||
@_spi(ForSwiftTestingOnly) @_spi(Experimental) public import Testing | ||
|
||
public import UniformTypeIdentifiers | ||
|
||
extension Attachment { | ||
/// Initialize an instance of this type that encloses the given image. | ||
/// | ||
/// - Parameters: | ||
/// - attachableValue: The value that will be attached to the output of | ||
/// the test run. | ||
/// - 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. | ||
/// - contentType: The image format with which to encode `attachableValue`. | ||
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), | ||
/// the result is undefined. Pass `nil` to let the testing library decide | ||
/// which image format to use. | ||
/// - encodingQuality: The encoding quality to use when encoding the image. | ||
/// If the image format used for encoding (specified by the `contentType` | ||
/// argument) does not support variable-quality encoding, the value of | ||
/// this argument is ignored. | ||
/// - sourceLocation: The source location of the call to this initializer. | ||
/// This value is used when recording issues associated with the | ||
/// attachment. | ||
/// | ||
/// This is the designated initializer for this type when attaching an image | ||
/// that conforms to ``AttachableAsCGImage``. | ||
fileprivate init<T>( | ||
attachableValue: T, | ||
named preferredName: String?, | ||
contentType: (any Sendable)?, | ||
encodingQuality: Float, | ||
sourceLocation: SourceLocation | ||
) where AttachableValue == _AttachableImageContainer<T> { | ||
var imageContainer = _AttachableImageContainer(image: attachableValue, encodingQuality: encodingQuality) | ||
|
||
// Update the preferred name to include an extension appropriate for the | ||
// given content type. (Note the `else` branch duplicates the logic in | ||
// `preferredContentType(forEncodingQuality:)` but will go away once our | ||
// minimum deployment targets include the UniformTypeIdentifiers framework.) | ||
var preferredName = preferredName ?? Self.defaultPreferredName | ||
if #available(_uttypesAPI, *) { | ||
let contentType: UTType = contentType | ||
.map { $0 as! UTType } | ||
.flatMap { contentType in | ||
if UTType.image.conforms(to: contentType) { | ||
// This type is an abstract base type of .image (or .image itself.) | ||
// We'll infer the concrete type based on other arguments. | ||
return nil | ||
} | ||
return contentType | ||
} ?? .preferred(forEncodingQuality: encodingQuality) | ||
preferredName = (preferredName as NSString).appendingPathExtension(for: contentType) | ||
imageContainer.contentType = contentType | ||
} else { | ||
// The caller can't provide a content type, so we'll pick one for them. | ||
let ext = if encodingQuality < 1.0 { | ||
"jpg" | ||
} else { | ||
"png" | ||
} | ||
if (preferredName as NSString).pathExtension.caseInsensitiveCompare(ext) != .orderedSame { | ||
preferredName = (preferredName as NSString).appendingPathExtension(ext) ?? preferredName | ||
} | ||
} | ||
|
||
self.init(imageContainer, named: preferredName, sourceLocation: sourceLocation) | ||
} | ||
|
||
/// Initialize an instance of this type that encloses the given image. | ||
/// | ||
/// - Parameters: | ||
/// - attachableValue: The value that will be attached to the output of | ||
/// the test run. | ||
/// - 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. | ||
/// - contentType: The image format with which to encode `attachableValue`. | ||
/// If this type does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image), | ||
/// the result is undefined. Pass `nil` to let the testing library decide | ||
/// which image format to use. | ||
/// - encodingQuality: The encoding quality to use when encoding the image. | ||
/// If the image format used for encoding (specified by the `contentType` | ||
/// argument) does not support variable-quality encoding, the value of | ||
/// this argument is ignored. | ||
/// - 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 | ||
/// ``AttachableAsCGImage`` protocol and can be attached to a test: | ||
/// | ||
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) | ||
@_spi(Experimental) | ||
@available(_uttypesAPI, *) | ||
public init<T>( | ||
_ attachableValue: T, | ||
named preferredName: String? = nil, | ||
as contentType: UTType?, | ||
encodingQuality: Float = 1.0, | ||
sourceLocation: SourceLocation = #_sourceLocation | ||
) where AttachableValue == _AttachableImageContainer<T> { | ||
self.init(attachableValue: attachableValue, named: preferredName, contentType: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation) | ||
} | ||
|
||
/// Initialize an instance of this type that encloses the given image. | ||
/// | ||
/// - Parameters: | ||
/// - attachableValue: The value that will be attached to the output of | ||
/// the test run. | ||
/// - 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. | ||
/// - encodingQuality: The encoding quality to use when encoding the image. | ||
/// If the image format used for encoding (specified by the `contentType` | ||
/// argument) does not support variable-quality encoding, the value of | ||
/// this argument is ignored. | ||
/// - 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 | ||
/// ``AttachableAsCGImage`` protocol and can be attached to a test: | ||
/// | ||
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage) | ||
@_spi(Experimental) | ||
public init<T>( | ||
_ attachableValue: T, | ||
named preferredName: String? = nil, | ||
encodingQuality: Float = 1.0, | ||
sourceLocation: SourceLocation = #_sourceLocation | ||
) where AttachableValue == _AttachableImageContainer<T> { | ||
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation) | ||
} | ||
} | ||
#endif |
20 changes: 20 additions & 0 deletions
20
Sources/Overlays/_Testing_CoreGraphics/Attachments/CGImage+AttachableAsCGImage.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// 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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics) | ||
public import CoreGraphics | ||
|
||
@_spi(Experimental) | ||
extension CGImage: AttachableAsCGImage { | ||
public var attachableCGImage: CGImage { | ||
self | ||
} | ||
} | ||
#endif |
41 changes: 41 additions & 0 deletions
41
Sources/Overlays/_Testing_CoreGraphics/Attachments/ImageAttachmentError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// | ||
// 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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics) | ||
/// A type representing an error that can occur when attaching an image. | ||
@_spi(ForSwiftTestingOnly) | ||
public enum ImageAttachmentError: Error, CustomStringConvertible { | ||
/// The specified content type did not conform to `.image`. | ||
case contentTypeDoesNotConformToImage | ||
|
||
/// The image could not be converted to an instance of `CGImage`. | ||
case couldNotCreateCGImage | ||
|
||
/// The image destination could not be created. | ||
case couldNotCreateImageDestination | ||
|
||
/// The image could not be converted. | ||
case couldNotConvertImage | ||
|
||
@_spi(ForSwiftTestingOnly) | ||
public var description: String { | ||
switch self { | ||
case .contentTypeDoesNotConformToImage: | ||
"The specified type does not represent an image format." | ||
case .couldNotCreateCGImage: | ||
"Could not create the corresponding Core Graphics image." | ||
case .couldNotCreateImageDestination: | ||
"Could not create the Core Graphics image destination to encode this image." | ||
case .couldNotConvertImage: | ||
"Could not convert the image to the specified format." | ||
} | ||
} | ||
} | ||
#endif |
Oops, something went wrong.