Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ extension NSImageRep {
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
extension NSImage: AttachableAsCGImage {
extension NSImage: AttachableAsImage, AttachableAsCGImage {
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
public var attachableCGImage: CGImage {
package var attachableCGImage: CGImage {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be the first time I've seen the package access control actually used in swift.

get throws {
let ctm = AffineTransform(scale: _attachmentScaleFactor) as NSAffineTransform
let ctm = AffineTransform(scale: attachmentScaleFactor) as NSAffineTransform
guard let result = cgImage(forProposedRect: nil, context: nil, hints: [.ctm: ctm]) else {
throw ImageAttachmentError.couldNotCreateCGImage
}
return result
}
}

public var _attachmentScaleFactor: CGFloat {
package var attachmentScaleFactor: CGFloat {
let maxRepWidth = representations.lazy
.map { CGFloat($0.pixelsWide) / $0.size.width }
.filter { $0 > 0.0 }
Expand Down
2 changes: 1 addition & 1 deletion Sources/Overlays/_Testing_AppKit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
add_library(_Testing_AppKit
Attachments/NSImage+AttachableAsCGImage.swift
Attachments/NSImage+AttachableAsImage.swift
ReexportTesting.swift)

target_link_libraries(_Testing_AppKit PUBLIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,21 @@
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
public import CoreGraphics
private import ImageIO
package import CoreGraphics
package import ImageIO
private import UniformTypeIdentifiers

/// A protocol describing images that can be converted to instances of
/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment).
/// [`Attachment`](https://developer.apple.com/documentation/testing/attachment)
/// and which can be represented as instances of [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage).
///
/// Instances of types conforming to this protocol do not themselves conform to
/// [`Attachable`](https://developer.apple.com/documentation/testing/attachable).
/// Instead, the testing library provides additional initializers on [`Attachment`](https://developer.apple.com/documentation/testing/attachment)
/// that take instances of such types and handle converting them to image data when needed.
///
/// You can attach instances of the following system-provided image types to a
/// test:
///
/// | Platform | Supported Types |
/// |-|-|
/// | macOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage) |
/// | iOS, watchOS, tvOS, and visionOS | [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage), [`CIImage`](https://developer.apple.com/documentation/coreimage/ciimage), [`UIImage`](https://developer.apple.com/documentation/uikit/uiimage) |
/// | Windows | [`HBITMAP`](https://learn.microsoft.com/en-us/windows/win32/gdi/bitmaps), [`HICON`](https://learn.microsoft.com/en-us/windows/win32/menurc/icons), [`IWICBitmapSource`](https://learn.microsoft.com/en-us/windows/win32/api/wincodec/nn-wincodec-iwicbitmapsource) (including its subclasses declared by Windows Imaging Component) |
///
/// 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.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
/// This protocol is not part of the public interface of the testing library. It
/// encapsulates Apple-specific logic for image attachments.
@available(_uttypesAPI, *)
public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype {
package protocol AttachableAsCGImage: AttachableAsImage, SendableMetatype {
/// An instance of `CGImage` representing this image.
///
/// - Throws: Any error that prevents the creation of an image.
///
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
var attachableCGImage: CGImage { get throws }

/// The orientation of the image.
Expand All @@ -53,38 +32,64 @@ public protocol AttachableAsCGImage: _AttachableAsImage, SendableMetatype {
/// `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 }
/// This property is not part of the public interface of the testing library.
/// It may be removed in a future update.
var attachmentOrientation: CGImagePropertyOrientation { 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 }
/// 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 }
}

@available(_uttypesAPI, *)
extension AttachableAsCGImage {
public var _attachmentOrientation: UInt32 {
CGImagePropertyOrientation.up.rawValue
package var attachmentOrientation: CGImagePropertyOrientation {
.up
}

public var _attachmentScaleFactor: CGFloat {
package var attachmentScaleFactor: CGFloat {
1.0
}

public func _deinitializeAttachableValue() {}
}
public func withUnsafeBytes<R>(as imageFormat: AttachableImageFormat, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let data = NSMutableData()

@available(_uttypesAPI, *)
extension AttachableAsCGImage where Self: Sendable {
public func _copyAttachableValue() -> Self {
self
// Convert the image to a CGImage.
let attachableCGImage = try attachableCGImage

// Create the image destination.
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, imageFormat.contentType.identifier as CFString, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
}

// Configure the properties of the image conversion operation.
let orientation = attachmentOrientation
let scaleFactor = attachmentScaleFactor
let properties: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat.encodingQuality),
kCGImagePropertyOrientation: orientation,
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
Comment on lines +77 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Pull this out to a variable? Name it imageDpi?

]

// Perform the image conversion.
CGImageDestinationAddImage(dest, attachableCGImage, properties as CFDictionary)
guard CGImageDestinationFinalize(dest) else {
throw ImageAttachmentError.couldNotConvertImage
}

// Pass the bits of the image out to the body. Note that we have an
// NSMutableData here so we have to use slightly different API than we would
// with an instance of Data.
return try withExtendedLifetime(data) {
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,58 +9,10 @@
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
public import Testing

public import UniformTypeIdentifiers

@available(_uttypesAPI, *)
extension AttachableImageFormat {
/// Get the content type to use when encoding the image, substituting a
/// concrete type for `UTType.image` in particular.
///
/// - 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 `UTType` referring to a concrete image type.
///
/// This function is not part of the public interface of the testing library.
static func computeContentType(for imageFormat: Self?, withPreferredName preferredName: String) -> UTType {
guard let imageFormat else {
// The developer didn't specify a type. Substitute the generic `.image`
// and solve for that instead.
return computeContentType(for: Self(.image, encodingQuality: 1.0), withPreferredName: preferredName)
}

switch imageFormat.kind {
case .png:
return .png
case .jpeg:
return .jpeg
case let .systemValue(contentType):
let contentType = contentType as! UTType
if contentType != .image {
// The developer explicitly specified a type.
return contentType
}

// The developer didn't specify a concrete type, so try to derive one from
// the preferred name's path extension.
let pathExtension = (preferredName as NSString).pathExtension
if !pathExtension.isEmpty,
let contentType = UTType(filenameExtension: pathExtension, conformingTo: .image),
contentType.isDeclared {
return contentType
}

// We couldn't derive a concrete type from the path extension, so pick
// between PNG and JPEG based on the encoding quality.
return imageFormat.encodingQuality < 1.0 ? .jpeg : .png
}
}

/// The content type corresponding to this image format.
///
/// For example, if this image format equals ``png``, the value of this
Expand All @@ -72,14 +24,7 @@ extension AttachableImageFormat {
/// @Available(Swift, introduced: 6.3)
/// }
public var contentType: UTType {
switch kind {
case .png:
return .png
case .jpeg:
return .jpeg
case let .systemValue(contentType):
return contentType as! UTType
}
kind.contentType
}

/// Initialize an instance of this type with the given content type and
Expand All @@ -100,12 +45,19 @@ extension AttachableImageFormat {
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
public init(_ contentType: UTType, encodingQuality: Float = 1.0) {
precondition(
contentType.conforms(to: .image),
"An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead."
)
self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality)
public init(contentType: UTType, encodingQuality: Float = 1.0) {
switch contentType {
case .png:
self.init(kind: .png, encodingQuality: encodingQuality)
case .jpeg:
self.init(kind: .jpeg, encodingQuality: encodingQuality)
default:
precondition(
contentType.conforms(to: .image),
"An image cannot be attached as an instance of type '\(contentType.identifier)'. Use a type that conforms to 'public.image' instead."
)
self.init(kind: .systemValue(contentType), encodingQuality: encodingQuality)
}
}

/// Construct an instance of this type with the given path extension and
Expand Down Expand Up @@ -135,11 +87,42 @@ extension AttachableImageFormat {
public init?(pathExtension: String, encodingQuality: Float = 1.0) {
let pathExtension = pathExtension.drop { $0 == "." }

guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image) else {
guard let contentType = UTType(filenameExtension: String(pathExtension), conformingTo: .image),
contentType.isDeclared else {
return nil
}

self.init(contentType, encodingQuality: encodingQuality)
self.init(contentType: contentType, encodingQuality: encodingQuality)
}
}

// MARK: - CustomStringConvertible, CustomDebugStringConvertible

@available(_uttypesAPI, *)
extension AttachableImageFormat.Kind: CustomStringConvertible, CustomDebugStringConvertible {
/// The content type corresponding to this image format.
fileprivate var contentType: UTType {
switch self {
case .png:
return .png
case .jpeg:
return .jpeg
case let .systemValue(contentType):
return contentType as! UTType
}
}

package var description: String {
let contentType = contentType
return contentType.localizedDescription ?? contentType.identifier
}

package var debugDescription: String {
let contentType = contentType
if let localizedDescription = contentType.localizedDescription {
return "\(localizedDescription) (\(contentType.identifier))"
}
return contentType.identifier
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ public import CoreGraphics
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
extension CGImage: AttachableAsCGImage {
extension CGImage: AttachableAsImage, AttachableAsCGImage {
/// @Metadata {
/// @Available(Swift, introduced: 6.3)
/// }
public var attachableCGImage: CGImage {
package var attachableCGImage: CGImage {
self
}
}
Expand Down
Loading