Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add an associated AttachmentMetadata type to Attachable. #824

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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 @@ -11,8 +11,6 @@
#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.
///
Expand All @@ -22,78 +20,9 @@ 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`.
/// 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.
/// - metadata: Optional metadata such as the image format to use when
/// encoding `image`. If `nil`, the testing library will infer the format
/// and other metadata.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
Expand All @@ -103,45 +32,14 @@ extension Attachment {
///
/// - [`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,
named preferredName: String?,
metadata: ImageAttachmentMetadata = .init(),
sourceLocation: SourceLocation = #_sourceLocation
) where AttachableValue == _AttachableImageContainer<T> {
self.init(attachableValue: attachableValue, named: preferredName, contentType: nil, encodingQuality: encodingQuality, sourceLocation: sourceLocation)
let imageContainer = _AttachableImageContainer(attachableValue)
self.init(imageContainer, named: preferredName, metadata: metadata, sourceLocation: sourceLocation)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// 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(Experimental) public import Testing
private import CoreGraphics

public import UniformTypeIdentifiers

/// A type defining metadata used when attaching an image to a test.
///
/// The following system-provided image types can be attached to a test:
///
/// - [`CGImage`](https://developer.apple.com/documentation/coregraphics/cgimage)
@_spi(Experimental)
public struct ImageAttachmentMetadata: Sendable {
/// The encoding quality to use when encoding the represented image.
///
/// If the image format used for encoding (specified by the ``contentType``
/// property) does not support variable-quality encoding, the value of this
/// property is ignored.
public var encodingQuality: Float

/// Storage for ``contentType``.
private var _contentType: (any Sendable)?

/// 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.
@available(_uttypesAPI, *)
public var contentType: UTType {
get {
if let contentType = _contentType as? UTType {
return contentType
} else if encodingQuality < 1.0 {
return .jpeg
} else {
return .png
}
}
set {
lazy var newValueDescription = newValue.localizedDescription ?? newValue.identifier
precondition(
newValue.conforms(to: .image),
"An image cannot be attached as an instance of type '\(newValueDescription)'. 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`.
@available(_uttypesAPI, *)
var computedContentType: UTType {
if let contentType = _contentType as? UTType, contentType != .image {
return contentType
} else if encodingQuality < 1.0 {
return .jpeg
} else {
return .png
}
}

/// The type identifier (as a `CFString`) corresponding to this instance's
/// ``contentType`` property.
///
/// The value of this property is used by ImageIO when serializing an image.
public var typeIdentifier: CFString {
if #available(_uttypesAPI, *) {
computedContentType.identifier as CFString
} else if encodingQuality < 1.0 {
kUTTypeJPEG
} else {
kUTTypePNG
}
}

public init(encodingQuality: Float = 1.0) {
self.encodingQuality = encodingQuality
}

@available(_uttypesAPI, *)
public init(encodingQuality: Float = 1.0, contentType: UTType) {
self.encodingQuality = encodingQuality
self.contentType = contentType
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
private import CoreGraphics

private import ImageIO
import UniformTypeIdentifiers
private import UniformTypeIdentifiers

#if canImport(CoreServices_Private)
private import CoreServices_Private
#endif

/// ## Why can't images directly conform to Attachable?
///
Expand All @@ -24,10 +28,7 @@ import UniformTypeIdentifiers
/// event handler (primarily because `Event` is `Sendable`.) So we would have
/// to eagerly serialize them, which is unnecessarily expensive if we know
/// they're actually concurrency-safe.
/// 2. We would have no place to store metadata such as the encoding quality
/// (although in the future we may introduce a "metadata" associated type to
/// `Attachable` that could store that info.)
/// 3. `Attachable` has a requirement with `Self` in non-parameter, non-return
/// 2. `Attachable` has a requirement with `Self` in non-parameter, non-return
/// position. As far as Swift is concerned, a non-final class cannot satisfy
/// such a requirement, and all image types we care about are non-final
/// classes. Thus, the compiler will steadfastly refuse to allow non-final
Expand Down Expand Up @@ -57,81 +58,28 @@ public struct _AttachableImageContainer<Image>: Sendable where Image: Attachable
/// 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.
public var encodingQuality: Float

/// Storage for ``contentType``.
private var _contentType: (any Sendable)?

/// The content type to use when encoding the image.
///
/// This property should eventually move up to ``Attachment``. It is not part
/// of the public interface of the testing library.
@available(_uttypesAPI, *)
var contentType: UTType? {
get {
_contentType as? UTType
}
set {
_contentType = newValue
}
}

init(image: Image, encodingQuality: Float) {
init(_ image: borrowing Image) {
self.image = image._makeCopyForAttachment()
self.encodingQuality = encodingQuality
}
}

// MARK: -

@available(_uttypesAPI, *)
extension UTType {
/// Determine the preferred content type to encode this image as for a given
/// encoding quality.
///
/// - Parameters:
/// - encodingQuality: The encoding quality to use when encoding the image.
///
/// - Returns: The type to encode this image as.
static func preferred(forEncodingQuality encodingQuality: Float) -> Self {
// If the caller wants lossy encoding, use JPEG.
if encodingQuality < 1.0 {
return .jpeg
}

// Lossless encoding implies PNG.
return .png
}
}

extension _AttachableImageContainer: AttachableContainer {
public var attachableValue: Image {
image
}

public typealias AttachmentMetadata = ImageAttachmentMetadata

public func withUnsafeBufferPointer<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R {
let data = NSMutableData()

// Convert the image to a CGImage.
let attachableCGImage = try image.attachableCGImage

// Get the type to encode as. (Note the `else` branches duplicate the logic
// in `preferredContentType(forEncodingQuality:)` but will go away once our
// minimum deployment targets include the UniformTypeIdentifiers framework.)
let typeIdentifier: CFString
if #available(_uttypesAPI, *), let contentType {
guard contentType.conforms(to: .image) else {
throw ImageAttachmentError.contentTypeDoesNotConformToImage
}
typeIdentifier = contentType.identifier as CFString
} else if encodingQuality < 1.0 {
typeIdentifier = kUTTypeJPEG
} else {
typeIdentifier = kUTTypePNG
}

// Create the image destination.
let typeIdentifier = attachment.metadata.typeIdentifier
guard let dest = CGImageDestinationCreateWithData(data as CFMutableData, typeIdentifier, 1, nil) else {
throw ImageAttachmentError.couldNotCreateImageDestination
}
Expand All @@ -140,7 +88,7 @@ extension _AttachableImageContainer: AttachableContainer {
let orientation = image._attachmentOrientation
let scaleFactor = image._attachmentScaleFactor
let properties: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: CGFloat(encodingQuality),
kCGImageDestinationLossyCompressionQuality: CGFloat(attachment.metadata.encodingQuality),
kCGImagePropertyOrientation: orientation,
kCGImagePropertyDPIWidth: 72.0 * scaleFactor,
kCGImagePropertyDPIHeight: 72.0 * scaleFactor,
Expand All @@ -159,5 +107,23 @@ extension _AttachableImageContainer: AttachableContainer {
try body(UnsafeRawBufferPointer(start: data.bytes, count: data.length))
}
}

public borrowing func makePreferredName(from suggestedName: String, for attachment: borrowing Attachment<Self>) -> String {
let preferredName = attachment.preferredName

if #available(_uttypesAPI, *) {
let metadata = attachment.metadata
let contentType = metadata.contentType
if contentType != .image {
return (preferredName as NSString).appendingPathExtension(for: metadata.contentType)
} else if metadata.encodingQuality < 1.0 {
return (preferredName as NSString).appendingPathExtension(for: .jpeg)
} else {
return (preferredName as NSString).appendingPathExtension(for: .png)
}
}

return preferredName
}
}
#endif
Loading