Skip to content
Merged
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 @@ -31,6 +31,7 @@ private import ImageIO
/// 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)
@available(_uttypesAPI, *)
public protocol AttachableAsCGImage {
/// An instance of `CGImage` representing this image.
///
Expand Down Expand Up @@ -73,6 +74,7 @@ public protocol AttachableAsCGImage {
func _makeCopyForAttachment() -> Self
}

@available(_uttypesAPI, *)
extension AttachableAsCGImage {
public var _attachmentOrientation: UInt32 {
CGImagePropertyOrientation.up.rawValue
Expand All @@ -83,6 +85,7 @@ extension AttachableAsCGImage {
}
}

@available(_uttypesAPI, *)
extension AttachableAsCGImage where Self: Sendable {
public func _makeCopyForAttachment() -> Self {
self
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// 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 SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
@_spi(Experimental) 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),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to self: on Windows, ImageCodecInfo::FilenameExtension should help us here. Windows does not have hierarchical type declarations (does it?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pointer to a null-terminated string that contains all file-name extensions associated with the codec. The extensions are separated by semicolons.

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.
///
/// The value of this property always conforms to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image).
public var contentType: UTType {
switch kind {
case .png:
return .png
case .jpeg:
return .jpeg
case let .systemValue(contentType):
return contentType as! UTType
}
}

/// Initialize an instance of this type with the given content type and
/// encoding quality.
///
/// - Parameters:
/// - contentType: 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 `imageFormat` is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
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)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
@_spi(Experimental) public import Testing

public import UniformTypeIdentifiers

@_spi(Experimental)
@available(_uttypesAPI, *)
extension Attachment {
Expand All @@ -24,10 +22,7 @@ extension Attachment {
/// - preferredName: The preferred name of the attachment when writing it
/// to a test report or to disk. If `nil`, the testing library attempts
/// to derive a reasonable filename for the attached value.
/// - contentType: The image format with which to encode `attachableValue`.
/// - encodingQuality: The encoding quality to use when encoding the image.
/// For the lowest supported quality, pass `0.0`. For the highest
/// supported quality, pass `1.0`.
/// - 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.
Expand All @@ -39,26 +34,20 @@ extension Attachment {
/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage)
/// (macOS)
///
/// The testing library uses the image format specified by `contentType`. Pass
/// 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.
///
/// If the target image format does not support variable-quality encoding,
/// the value of the `encodingQuality` argument is ignored. If `contentType`
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
public init<T>(
_ attachableValue: T,
named preferredName: String? = nil,
as contentType: UTType? = nil,
encodingQuality: Float = 1.0,
as imageFormat: AttachableImageFormat? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
let imageWrapper = _AttachableImageWrapper(image: attachableValue, encodingQuality: encodingQuality, contentType: contentType)
let imageWrapper = _AttachableImageWrapper(image: attachableValue, imageFormat: imageFormat)
self.init(imageWrapper, named: preferredName, sourceLocation: sourceLocation)
}

Expand All @@ -69,10 +58,7 @@ extension Attachment {
/// - preferredName: The preferred name of the attachment when writing it to
/// a test report or to disk. If `nil`, the testing library attempts to
/// derive a reasonable filename for the attached value.
/// - contentType: The image format with which to encode `attachableValue`.
/// - encodingQuality: The encoding quality to use when encoding the image.
/// For the lowest supported quality, pass `0.0`. For the highest
/// supported quality, pass `1.0`.
/// - imageFormat: The image format with which to encode `attachableValue`.
/// - sourceLocation: The source location of the call to this function.
///
/// This function creates a new instance of ``Attachment`` wrapping `image`
Expand All @@ -85,26 +71,20 @@ extension Attachment {
/// - [`NSImage`](https://developer.apple.com/documentation/appkit/nsimage)
/// (macOS)
///
/// The testing library uses the image format specified by `contentType`. Pass
/// 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.
///
/// If the target image format does not support variable-quality encoding,
/// the value of the `encodingQuality` argument is ignored. If `contentType`
/// is not `nil` and does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
public static func record<T>(
_ image: consuming T,
named preferredName: String? = nil,
as contentType: UTType? = nil,
encodingQuality: Float = 1.0,
as imageFormat: AttachableImageFormat? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageWrapper<T> {
let attachment = Self(image, named: preferredName, as: contentType, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
let attachment = Self(image, named: preferredName, as: imageFormat, sourceLocation: sourceLocation)
Self.record(attachment, sourceLocation: sourceLocation)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//

#if SWT_TARGET_OS_APPLE && canImport(CoreGraphics)
public import Testing
@_spi(Experimental) public import Testing
private import CoreGraphics

private import ImageIO
Expand Down Expand Up @@ -60,49 +60,12 @@ public struct _AttachableImageWrapper<Image>: Sendable where Image: AttachableAs
/// instances of this type it creates hold "safe" `NSImage` instances.
nonisolated(unsafe) var image: Image

/// The encoding quality to use when encoding the represented image.
var encodingQuality: Float
/// The image format to use when encoding the represented image.
var imageFormat: AttachableImageFormat?

/// Storage for ``contentType``.
private var _contentType: UTType?

/// The content type to use when encoding the image.
///
/// The testing library uses this property to determine which image format to
/// encode the associated image as when it is attached to a test.
///
/// If the value of this property does not conform to [`UTType.image`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/image),
/// the result is undefined.
var contentType: UTType {
get {
_contentType ?? .image
}
set {
precondition(
newValue.conforms(to: .image),
"An image cannot be attached as an instance of type '\(newValue.identifier)'. Use a type that conforms to 'public.image' instead."
)
_contentType = newValue
}
}

/// The content type to use when encoding the image, substituting a concrete
/// type for `UTType.image`.
///
/// This property is not part of the public interface of the testing library.
var computedContentType: UTType {
if contentType == .image {
return encodingQuality < 1.0 ? .jpeg : .png
}
return contentType
}

init(image: Image, encodingQuality: Float, contentType: UTType?) {
init(image: Image, imageFormat: AttachableImageFormat?) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
if let contentType {
self.contentType = contentType
}
self.imageFormat = imageFormat
}
}

Expand All @@ -121,16 +84,16 @@ extension _AttachableImageWrapper: AttachableWrapper {
let attachableCGImage = try image.attachableCGImage

// Create the image destination.
let typeIdentifier = computedContentType.identifier as CFString
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: attachment.preferredName)
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, contentType.identifier as CFString, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
}

// Configure the properties of the image conversion operation.
let orientation = image._attachmentOrientation
let scaleFactor = image._attachmentScaleFactor
let properties: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: CGFloat(encodingQuality),
kCGImageDestinationLossyCompressionQuality: CGFloat(imageFormat?.encodingQuality ?? 1.0),
kCGImagePropertyOrientation: orientation,
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
Expand All @@ -151,7 +114,8 @@ extension _AttachableImageWrapper: AttachableWrapper {
}

public borrowing func preferredName(for attachment: borrowing Attachment<Self>, basedOn suggestedName: String) -> String {
(suggestedName as NSString).appendingPathExtension(for: computedContentType)
let contentType = AttachableImageFormat.computeContentType(for: imageFormat, withPreferredName: suggestedName)
return (suggestedName as NSString).appendingPathExtension(for: contentType)
}
}
#endif
Loading