Skip to content
30 changes: 29 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 35 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
// swift-tools-version:5.7
// swift-tools-version:5.10

import PackageDescription

let package = Package(
name: "swift-snapshot-testing",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.macOS(.v12),
.tvOS(.v13),
.watchOS(.v6),
.watchOS(.v8),
],
products: [
.library(
name: "SnapshotTesting",
targets: ["SnapshotTesting"]
),
.library(
name: "JPEGXLImageSerializer",
targets: ["JPEGXLImageSerializer"]
),
.library(
name: "WEBPImageSerializer",
targets: ["WEBPImageSerializer"]
),
.library(
name: "InlineSnapshotTesting",
targets: ["InlineSnapshotTesting"]
),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease")
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"),
.package(url: "https://github.com/awxkee/jxl-coder-swift.git", from: "1.7.3"),
.package(url: "https://github.com/awxkee/webp.swift.git", from: "1.1.1"),
],
targets: [
.target(
name: "SnapshotTesting"
name: "SnapshotTesting",
dependencies: [
"ImageSerializer"
]
),
.target(
name: "ImageSerializer"
),
.target(
name: "JPEGXLImageSerializer",
dependencies: [
"ImageSerializer",
.product(name: "JxlCoder", package: "jxl-coder-swift")
]
),
.target(
name: "WEBPImageSerializer",
dependencies: [
"ImageSerializer",
.product(name: "webp", package: "webp.swift")
]
),
.target(
name: "InlineSnapshotTesting",
Expand Down
54 changes: 54 additions & 0 deletions Sources/ImageSerializer/ImageSerializer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

#if !os(macOS)
import UIKit.UIImage
/// A type alias for `UIImage` on iOS and `NSImage` on macOS.
package typealias SnapImage = UIImage
#else
import AppKit.NSImage
/// A type alias for `UIImage` on iOS and `NSImage` on macOS.
package typealias SnapImage = NSImage
#endif

/// A structure responsible for encoding and decoding images.
///
/// The `ImageSerializer` structure provides two closures:
/// - `encodeImage`: Encodes a `SnapImage` into `Data`.
/// - `decodeImage`: Decodes `Data` back into a `SnapImage`.
///
/// These closures allow you to define custom image serialization logic for different image formats.
package struct ImageSerializer {
/// A closure that encodes a `SnapImage` into `Data`.
package var encodeImage: (_ image: SnapImage) -> Data?

/// A closure that decodes `Data` into a `SnapImage`.
package var decodeImage: (_ data: Data) -> SnapImage?

/// Initializes an `ImageSerializer` with custom encoding and decoding logic.
///
/// - Parameters:
/// - encodeImage: A closure that defines how to encode a `SnapImage` into `Data`.
/// - decodeImage: A closure that defines how to decode `Data` into a `SnapImage`.
package init(encodeImage: @escaping (_: SnapImage) -> Data?, decodeImage: @escaping (_: Data) -> SnapImage?) {
self.encodeImage = encodeImage
self.decodeImage = decodeImage
}
}

/// An enumeration of supported image formats.
///
/// `ImageFormat` defines the formats that can be used for image serialization:
/// - `.jxl`: JPEG XL format.
/// - `.png`: PNG format.
/// - `.heic`: HEIC format.
/// - `.webp`: WEBP format.
///
/// The `defaultValue` is set to `.png`.
public enum ImageFormat: String {
case jxl
case png
case heic
case webp

public static var defaultValue = ImageFormat.png
}
25 changes: 25 additions & 0 deletions Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation
import JxlCoder
import ImageSerializer

extension ImageSerializer {
/// A static property that provides an `ImageSerializer` for the JPEG XL format.
///
/// This property uses the `JXLCoder` to encode and decode images in the JPEG XL format.
///
/// - Returns: An `ImageSerializer` instance configured for encoding and decoding JPEG XL images.
///
/// - Encoding:
/// - The `encodeImage` closure uses `JXLCoder.encode(image:)` to convert a `SnapImage` into `Data`.
/// - Decoding:
/// - The `decodeImage` closure uses `JXLCoder.decode(data:)` to convert `Data` back into a `SnapImage`.
///
/// - Note: The encoding and decoding operations are performed using the `JXLCoder` library, which supports the JPEG XL format.
package static var jxl: Self {
return ImageSerializer { image in
try? JXLCoder.encode(image: image)
} decodeImage: { data in
try? JXLCoder.decode(data: data)
}
}
}
3 changes: 3 additions & 0 deletions Sources/SnapshotTesting/AssertSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public var __record: SnapshotTestingConfiguration.Record = {
return .missing
}()

/// We can set the image format globally to better test
public var imageFormat = ImageFormat.defaultValue

/// Asserts that a given value matches a reference on disk.
///
/// - Parameters:
Expand Down
12 changes: 6 additions & 6 deletions Sources/SnapshotTesting/Snapshotting/CALayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// assertSnapshot(of: layer, as: .image(precision: 0.99))
/// ```
public static var image: Snapshotting {
return .image(precision: 1)
return .image(precision: 1, format: imageFormat)
}

/// A snapshot strategy for comparing layers based on pixel equality.
Expand All @@ -25,9 +25,9 @@
/// match. 98-99% mimics
/// [the precision](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e) of the
/// human eye.
public static func image(precision: Float, perceptualPrecision: Float = 1) -> Snapshotting {
public static func image(precision: Float, perceptualPrecision: Float = 1, format: ImageFormat) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision
precision: precision, perceptualPrecision: perceptualPrecision, format: format
).pullback { layer in
let image = NSImage(size: layer.bounds.size)
image.lockFocus()
Expand All @@ -46,7 +46,7 @@
extension Snapshotting where Value == CALayer, Format == UIImage {
/// A snapshot strategy for comparing layers based on pixel equality.
public static var image: Snapshotting {
return .image()
return .image(format: imageFormat)
}

/// A snapshot strategy for comparing layers based on pixel equality.
Expand All @@ -59,12 +59,12 @@
/// human eye.
/// - traits: A trait collection override.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, traits: UITraitCollection = .init()
precision: Float = 1, perceptualPrecision: Float = 1, traits: UITraitCollection = .init(), format: ImageFormat
)
-> Snapshotting
{
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale
precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale, format: format
).pullback { layer in
renderer(bounds: layer.bounds, for: traits).image { ctx in
layer.setNeedsLayout()
Expand Down
15 changes: 9 additions & 6 deletions Sources/SnapshotTesting/Snapshotting/CGPath.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#if os(macOS)

import AppKit
import Cocoa
import CoreGraphics

extension Snapshotting where Value == CGPath, Format == NSImage {
/// A snapshot strategy for comparing bezier paths based on pixel equality.
public static var image: Snapshotting {
return .image()
return .image(format: imageFormat)
}

/// A snapshot strategy for comparing bezier paths based on pixel equality.
Expand All @@ -29,10 +30,11 @@
public static func image(
precision: Float = 1,
perceptualPrecision: Float = 1,
drawingMode: CGPathDrawingMode = .eoFill
drawingMode: CGPathDrawingMode = .eoFill,
format: ImageFormat
) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision
precision: precision, perceptualPrecision: perceptualPrecision, format: format
).pullback { path in
let bounds = path.boundingBoxOfPath
var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y)
Expand All @@ -52,10 +54,11 @@
#elseif os(iOS) || os(tvOS)
import UIKit


extension Snapshotting where Value == CGPath, Format == UIImage {
/// A snapshot strategy for comparing bezier paths based on pixel equality.
public static var image: Snapshotting {
return .image()
return .image(format: imageFormat)
}

/// A snapshot strategy for comparing bezier paths based on pixel equality.
Expand All @@ -68,10 +71,10 @@
/// human eye.
public static func image(
precision: Float = 1, perceptualPrecision: Float = 1, scale: CGFloat = 1,
drawingMode: CGPathDrawingMode = .eoFill
drawingMode: CGPathDrawingMode = .eoFill, format: ImageFormat
) -> Snapshotting {
return SimplySnapshotting.image(
precision: precision, perceptualPrecision: perceptualPrecision, scale: scale
precision: precision, perceptualPrecision: perceptualPrecision, scale: scale, format: format
).pullback { path in
let bounds = path.boundingBoxOfPath
let format: UIGraphicsImageRendererFormat
Expand Down
76 changes: 76 additions & 0 deletions Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Foundation
import ImageIO
import UniformTypeIdentifiers
import ImageSerializer

#if canImport(UIKit)
import UIKit
#endif
#if canImport(AppKit)
import AppKit
#endif

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
/// A struct that provides encoding and decoding functionality for HEIC images.
///
/// `HEICCoder` supports encoding images to HEIC format and decoding HEIC data back into images.
///
/// - Note: The HEIC format is only supported on iOS 14.0+ and macOS 10.15+.
struct HEICCoder {
/// Encodes a `SnapImage` into HEIC format.
///
/// This method converts a `SnapImage` to `Data` using the HEIC format.
///
/// - Parameter image: The image to be encoded. This can be a `UIImage` on iOS or an `NSImage` on macOS.
///
/// - Returns: The encoded image data in HEIC format, or `nil` if encoding fails.
///
/// - Note: The encoding quality is set to 0.8 (lossy compression). On macOS, the image is created using `CGImageDestinationCreateWithData`.
static func encodeImage(_ image: SnapImage) -> Data? {
#if !os(macOS)
guard let cgImage = image.cgImage else { return nil }
#else
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
#endif

let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data, UTType.heic.identifier as CFString, 1, nil) else { return nil }
CGImageDestinationAddImage(destination, cgImage, [kCGImageDestinationLossyCompressionQuality: 0.8] as CFDictionary)
guard CGImageDestinationFinalize(destination) else { return nil }
return data as Data
}

/// Decodes HEIC image data into a `SnapImage`.
///
/// This method converts HEIC image data back into a `SnapImage`.
///
/// - Parameter data: The HEIC data to be decoded.
///
/// - Returns: The decoded image as `SnapImage`, or `nil` if decoding fails.
///
/// - Note: On iOS, this returns a `UIImage`, while on macOS, it returns an `NSImage`.
static func decodeImage(_ data: Data) -> SnapImage? {
#if !os(macOS)
return UIImage(data: data)
#else
return NSImage(data: data)
#endif
}
}

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension ImageSerializer {
/// A static property that provides an `ImageSerializer` configured for HEIC format.
///
/// This property creates an `ImageSerializer` instance that uses `HEICCoder` to handle encoding and decoding of HEIC images.
///
/// - Returns: An `ImageSerializer` instance configured for HEIC format.
///
/// - Note: This property is available only on iOS 14.0 and later.
package static var heic: ImageSerializer {
ImageSerializer(
encodeImage: HEICCoder.encodeImage,
decodeImage: HEICCoder.decodeImage
)
}
}
Loading