From 641ad58daf62c4577dbcb23a80b1e437a459daf6 Mon Sep 17 00:00:00 2001 From: Moritz Sternemann Date: Tue, 1 Sep 2020 19:49:31 +0200 Subject: [PATCH 01/12] Make image diffing strategies custom scale aware (#336) * Add scale parameter to image diffing strategies * Use updated diffing in CALayer, UIView, UIViewController and SwiftUIView strategies * Pass scale through to CGPath and UIBezierPath strategies * Re-generate project file --- .../Snapshotting/CALayer.swift | 2 +- .../SnapshotTesting/Snapshotting/CGPath.swift | 2 +- .../Snapshotting/SwiftUIView.swift | 2 +- .../Snapshotting/UIBezierPath.swift | 2 +- .../Snapshotting/UIImage.swift | 21 +++++++++++++------ .../SnapshotTesting/Snapshotting/UIView.swift | 2 +- .../Snapshotting/UIViewController.swift | 4 ++-- 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/CALayer.swift b/Sources/SnapshotTesting/Snapshotting/CALayer.swift index 200c76d46..d11e37f4d 100644 --- a/Sources/SnapshotTesting/Snapshotting/CALayer.swift +++ b/Sources/SnapshotTesting/Snapshotting/CALayer.swift @@ -37,7 +37,7 @@ extension Snapshotting where Value == CALayer, Format == UIImage { /// - Parameter precision: The percentage of pixels that must match. public static func image(precision: Float = 1, traits: UITraitCollection = .init()) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { layer in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in renderer(bounds: layer.bounds, for: traits).image { ctx in layer.setNeedsLayout() layer.layoutIfNeeded() diff --git a/Sources/SnapshotTesting/Snapshotting/CGPath.swift b/Sources/SnapshotTesting/Snapshotting/CGPath.swift index 7435a4794..d7ee7df19 100644 --- a/Sources/SnapshotTesting/Snapshotting/CGPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/CGPath.swift @@ -40,7 +40,7 @@ extension Snapshotting where Value == CGPath, Format == UIImage { /// /// - Parameter precision: The percentage of pixels that must match. public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in let bounds = path.boundingBoxOfPath let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index 212c8a365..ec7bd9103 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -51,7 +51,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { config = .init(safeArea: .zero, size: size, traits: traits) } - return SimplySnapshotting.image(precision: precision).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in var config = config let controller: UIViewController diff --git a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift index 8981c2ad1..826044f4f 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift @@ -11,7 +11,7 @@ extension Snapshotting where Value == UIBezierPath, Format == UIImage { /// /// - Parameter precision: The percentage of pixels that must match. public static func image(precision: Float = 1, scale: CGFloat = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in let bounds = path.bounds let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 318b107ee..b5ff68048 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -4,16 +4,24 @@ import XCTest extension Diffing where Value == UIImage { /// A pixel-diffing strategy for UIImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1) + public static let image = Diffing.image(precision: 1, scale: nil) /// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be. /// /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. + /// - Parameter scale: Scale to use when loading the reference image from disk. If `nil` or the `UITraitCollection`s default value of `0.0`, the screens scale is used. /// - Returns: A new diffing strategy. - public static func image(precision: Float) -> Diffing { + public static func image(precision: Float, scale: CGFloat?) -> Diffing { + let imageScale: CGFloat + if let scale = scale, scale != 0.0 { + imageScale = scale + } else { + imageScale = UIScreen.main.scale + } + return Diffing( toData: { $0.pngData() ?? emptyImage().pngData()! }, - fromData: { UIImage(data: $0, scale: UIScreen.main.scale)! } + fromData: { UIImage(data: $0, scale: imageScale)! } ) { old, new in guard !compare(old, new, precision: precision) else { return nil } let difference = SnapshotTesting.diff(old, new) @@ -48,16 +56,17 @@ extension Diffing where Value == UIImage { extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1) + return .image(precision: 1, scale: nil) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float) -> Snapshotting { + /// - Parameter scale: The scale of the reference image stored on disk. + public static func image(precision: Float, scale: CGFloat?) -> Snapshotting { return .init( pathExtension: "png", - diffing: .image(precision: precision) + diffing: .image(precision: precision, scale: scale) ) } } diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index b66212881..fe1e81a5a 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -22,7 +22,7 @@ extension Snapshotting where Value == UIView, Format == UIImage { ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in snapshotView( config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index d78ad13b7..45e719cbf 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -22,7 +22,7 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) } ?? config, drawHierarchyInKeyWindow: false, @@ -48,7 +48,7 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in snapshotView( config: .init(safeArea: .zero, size: size, traits: traits), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, From 4579af4118879be3a9c4d48ca0fd31b623af95bc Mon Sep 17 00:00:00 2001 From: John Flanagan <91548783+jflan-dd@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:56:15 -0600 Subject: [PATCH 02/12] Add PreviewSnapshots to "Plug-ins" (#695) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5c9195a36..f74be1b19 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,8 @@ end - [GRDBSnapshotTesting](https://github.com/SebastianOsinski/GRDBSnapshotTesting) adds snapshot strategy for testing SQLite database migrations made with [GRDB](https://github.com/groue/GRDB.swift). - [AccessibilitySnapshot+SnapshotTesting](https://github.com/Sherlouk/AccessibilitySnapshot-SnapshotTesting) adds [AccessibilitySnapshot](https://github.com/cashapp/AccessibilitySnapshot) support for SnapshotTesting. + + - [PreviewSnapshots](https://github.com/doordash-oss/swiftui-preview-snapshots) share `View` configurations between SwiftUI Previews and snapshot tests and generate several snapshots with a single test assertion. Have you written your own SnapshotTesting plug-in? [Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and submit a pull request! From 870a891af98c5e3425b95eebf633d8fa05ecae2f Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Wed, 4 Sep 2024 23:36:39 +0200 Subject: [PATCH 03/12] feat: add basic support for JPEG XL --- Package.swift | 15 +++-- .../Snapshotting/CALayer.swift | 12 ++-- .../SnapshotTesting/Snapshotting/CGPath.swift | 12 ++-- .../Snapshotting/ImageSerializer.swift | 59 +++++++++++++++++++ .../Snapshotting/NSBezierPath.swift | 6 +- .../Snapshotting/NSImage.swift | 29 ++++----- .../SnapshotTesting/Snapshotting/NSView.swift | 6 +- .../Snapshotting/NSViewController.swift | 6 +- .../Snapshotting/SceneKit.swift | 12 ++-- .../Snapshotting/SpriteKit.swift | 12 ++-- .../Snapshotting/SwiftUIView.swift | 7 ++- .../Snapshotting/UIBezierPath.swift | 6 +- .../Snapshotting/UIImage.swift | 22 +++---- .../SnapshotTesting/Snapshotting/UIView.swift | 9 +-- .../Snapshotting/UIViewController.swift | 16 ++--- 15 files changed, 145 insertions(+), 84 deletions(-) create mode 100644 Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift diff --git a/Package.swift b/Package.swift index 0518486d2..9e5aeacb8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,23 +1,28 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.10 import Foundation import PackageDescription let package = Package( name: "SnapshotTesting", platforms: [ - .iOS(.v11), - .macOS(.v10_10), - .tvOS(.v10) + .iOS(.v13), + .macOS(.v12), + .tvOS(.v13) ], products: [ .library( name: "SnapshotTesting", targets: ["SnapshotTesting"]), ], + dependencies: [ + .package(url: "https://github.com/awxkee/jxl-coder-swift.git", from: "1.7.3") + ], targets: [ .target( name: "SnapshotTesting", - dependencies: []), + dependencies: [ + .product(name: "JxlCoder", package: "jxl-coder-swift") + ]), .testTarget( name: "SnapshotTestingTests", dependencies: ["SnapshotTesting"]), diff --git a/Sources/SnapshotTesting/Snapshotting/CALayer.swift b/Sources/SnapshotTesting/Snapshotting/CALayer.swift index d11e37f4d..817f040e2 100644 --- a/Sources/SnapshotTesting/Snapshotting/CALayer.swift +++ b/Sources/SnapshotTesting/Snapshotting/CALayer.swift @@ -4,14 +4,14 @@ import Cocoa extension Snapshotting where Value == CALayer, Format == NSImage { /// A snapshot strategy for comparing layers based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1) + return .image(precision: 1, format: .defaultValue) } /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { layer in + public static func image(precision: Float, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, format: format).pullback { layer in let image = NSImage(size: layer.bounds.size) image.lockFocus() let context = NSGraphicsContext.current!.cgContext @@ -29,15 +29,15 @@ import UIKit 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: .defaultValue) } /// A snapshot strategy for comparing layers based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, traits: UITraitCollection = .init()) + public static func image(precision: Float = 1, traits: UITraitCollection = .init(), format: ImageFormat) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale, format: format).pullback { layer in renderer(bounds: layer.bounds, for: traits).image { ctx in layer.setNeedsLayout() layer.layoutIfNeeded() diff --git a/Sources/SnapshotTesting/Snapshotting/CGPath.swift b/Sources/SnapshotTesting/Snapshotting/CGPath.swift index d7ee7df19..7c44b7ea9 100644 --- a/Sources/SnapshotTesting/Snapshotting/CGPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/CGPath.swift @@ -4,14 +4,14 @@ import Cocoa 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: .defaultValue) } /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + public static func image(precision: Float = 1, drawingMode: CGPathDrawingMode = .eoFill, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, format: format).pullback { path in let bounds = path.boundingBoxOfPath var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y) let path = path.copy(using: &transform)! @@ -33,14 +33,14 @@ 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: .defaultValue) } /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in + public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, scale: scale, format: format).pullback { path in let bounds = path.boundingBoxOfPath let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift new file mode 100644 index 000000000..f9c18deef --- /dev/null +++ b/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift @@ -0,0 +1,59 @@ +import Foundation +import JxlCoder + +#if !os(macOS) +import UIKit.UIImage +public typealias SnapImage = UIImage + +private func EncodePNGImage(_ image: SnapImage) -> Data? { + return image.pngData() +} + +private func DecodePNGImage(_ data: Data) -> SnapImage? { + UIImage(data: data) +} + +#else +import AppKit.NSImage +public typealias SnapImage = NSImage + +private func EncodePNGImage(_ image: SnapImage) -> Data? { + guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } + let rep = NSBitmapImageRep(cgImage: cgImage) + rep.size = image.size + return rep.representation(using: .png, properties: [:]) +} + +private func DecodePNGImage(_ data: Data) -> SnapImage? { + NSImage(data: data) +} + +#endif + +package protocol DefaultValueProvider { + associatedtype Value + + static var defaultValue: Value { get } +} + +public enum ImageFormat: String, DefaultValueProvider { + case jxl + case png + + public static var defaultValue = ImageFormat.png +} + +package func EncodeImage(image: SnapImage, _ format: ImageFormat) -> Data? { + switch format { + case .jxl: return try? JXLCoder.encode(image: image) + case .png: return EncodePNGImage(image) + } +} + +package func DecodeImage(data: Data, _ format: ImageFormat) -> SnapImage? { + switch format { + case .jxl: return try? JXLCoder.decode(data: data) + case .png: return DecodePNGImage(data) + } +} + diff --git a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift index d9d5defce..541ae3bfa 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift @@ -4,14 +4,14 @@ import Cocoa extension Snapshotting where Value == NSBezierPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).pullback { path in + public static func image(precision: Float = 1, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, format: format).pullback { path in // Move path info frame: let bounds = path.bounds let transform = AffineTransform(translationByX: -bounds.origin.x, byY: -bounds.origin.y) diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index 9dd7a4e92..433b88ab8 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -4,18 +4,18 @@ import XCTest extension Diffing where Value == NSImage { /// A pixel-diffing strategy for NSImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1) + public static let image = Diffing.image(precision: 1, format: .defaultValue) /// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be. /// /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. /// - Returns: A new diffing strategy. - public static func image(precision: Float) -> Diffing { + public static func image(precision: Float, format: ImageFormat/* = .png*/) -> Diffing { return .init( - toData: { NSImagePNGRepresentation($0)! }, - fromData: { NSImage(data: $0)! } + toData: { EncodeImage(image: $0, format)! }, + fromData: { DecodeImage(data: $0, format)! } ) { old, new in - guard !compare(old, new, precision: precision) else { return nil } + guard !compare(old, new, precision: precision, format: format) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -31,28 +31,21 @@ extension Diffing where Value == NSImage { extension Snapshotting where Value == NSImage, Format == NSImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1) + return .image(precision: 1, format: .defaultValue) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float) -> Snapshotting { + public static func image(precision: Float, format: ImageFormat/* = .png*/) -> Snapshotting { return .init( - pathExtension: "png", - diffing: .image(precision: precision) + pathExtension: format.rawValue, + diffing: .image(precision: precision, format: format) ) } } -private func NSImagePNGRepresentation(_ image: NSImage) -> Data? { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } - let rep = NSBitmapImageRep(cgImage: cgImage) - rep.size = image.size - return rep.representation(using: .png, properties: [:]) -} - -private func compare(_ old: NSImage, _ new: NSImage, precision: Float) -> Bool { +private func compare(_ old: NSImage, _ new: NSImage, precision: Float, format: ImageFormat) -> Bool { guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard oldCgImage.width != 0 else { return false } @@ -67,7 +60,7 @@ private func compare(_ old: NSImage, _ new: NSImage, precision: Float) -> Bool { guard let newData = newContext.data else { return false } let byteCount = oldContext.height * oldContext.bytesPerRow if memcmp(oldData, newData, byteCount) == 0 { return true } - let newer = NSImage(data: NSImagePNGRepresentation(new)!)! + let newer = NSImage(data: EncodeImage(image: new, format)!)! guard let newerCgImage = newer.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } guard let newerContext = context(for: newerCgImage) else { return false } guard let newerData = newerContext.data else { return false } diff --git a/Sources/SnapshotTesting/Snapshotting/NSView.swift b/Sources/SnapshotTesting/Snapshotting/NSView.swift index 292570f2c..7fcfa1dbd 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSView.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSView.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == NSView, Format == NSImage { /// A snapshot strategy for comparing views based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing views based on pixel equality. @@ -12,8 +12,8 @@ extension Snapshotting where Value == NSView, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { - return SimplySnapshotting.image(precision: precision).asyncPullback { view in + public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, format: .defaultValue).asyncPullback { view in let initialSize = view.frame.size if let size = size { view.frame.size = size } guard view.frame.width > 0, view.frame.height > 0 else { diff --git a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift index 70d972478..e8a5150a4 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == NSViewController, Format == NSImage { /// A snapshot strategy for comparing view controller views based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing view controller views based on pixel equality. @@ -12,8 +12,8 @@ extension Snapshotting where Value == NSViewController, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting { - return Snapshotting.image(precision: precision, size: size).pullback { $0.view } + public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat) -> Snapshotting { + return Snapshotting.image(precision: precision, size: size, format: format).pullback { $0.view } } } diff --git a/Sources/SnapshotTesting/Snapshotting/SceneKit.swift b/Sources/SnapshotTesting/Snapshotting/SceneKit.swift index 86dc7ff53..dfcbb6d4c 100644 --- a/Sources/SnapshotTesting/Snapshotting/SceneKit.swift +++ b/Sources/SnapshotTesting/Snapshotting/SceneKit.swift @@ -13,8 +13,8 @@ extension Snapshotting where Value == SCNScene, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: The size of the scene. - public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { - return .scnScene(precision: precision, size: size) + public static func image(precision: Float = 1, size: CGSize, format: ImageFormat) -> Snapshotting { + return .scnScene(precision: precision, size: size, format: format) } } #elseif os(iOS) || os(tvOS) @@ -24,15 +24,15 @@ extension Snapshotting where Value == SCNScene, Format == UIImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: The size of the scene. - public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { - return .scnScene(precision: precision, size: size) + public static func image(precision: Float = 1, size: CGSize, format: ImageFormat) -> Snapshotting { + return .scnScene(precision: precision, size: size, format: format) } } #endif fileprivate extension Snapshotting where Value == SCNScene, Format == Image { - static func scnScene(precision: Float, size: CGSize) -> Snapshotting { - return Snapshotting.image(precision: precision).pullback { scene in + static func scnScene(precision: Float, size: CGSize, format: ImageFormat) -> Snapshotting { + return Snapshotting.image(precision: precision, format: format).pullback { scene in let view = SCNView(frame: .init(x: 0, y: 0, width: size.width, height: size.height)) view.scene = scene return view diff --git a/Sources/SnapshotTesting/Snapshotting/SpriteKit.swift b/Sources/SnapshotTesting/Snapshotting/SpriteKit.swift index 8d71ce131..2751b0ac4 100644 --- a/Sources/SnapshotTesting/Snapshotting/SpriteKit.swift +++ b/Sources/SnapshotTesting/Snapshotting/SpriteKit.swift @@ -13,8 +13,8 @@ extension Snapshotting where Value == SKScene, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: The size of the scene. - public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { - return .skScene(precision: precision, size: size) + public static func image(precision: Float = 1, size: CGSize, format: ImageFormat) -> Snapshotting { + return .skScene(precision: precision, size: size, format: format) } } #elseif os(iOS) || os(tvOS) @@ -24,15 +24,15 @@ extension Snapshotting where Value == SKScene, Format == UIImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: The size of the scene. - public static func image(precision: Float = 1, size: CGSize) -> Snapshotting { - return .skScene(precision: precision, size: size) + public static func image(precision: Float = 1, size: CGSize, format: ImageFormat) -> Snapshotting { + return .skScene(precision: precision, size: size, format: format) } } #endif fileprivate extension Snapshotting where Value == SKScene, Format == Image { - static func skScene(precision: Float, size: CGSize) -> Snapshotting { - return Snapshotting.image(precision: precision).pullback { scene in + static func skScene(precision: Float, size: CGSize, format: ImageFormat) -> Snapshotting { + return Snapshotting.image(precision: precision, format: format).pullback { scene in let view = SKView(frame: .init(x: 0, y: 0, width: size.width, height: size.height)) view.presentScene(scene) return view diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index ec7bd9103..711e17624 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -20,7 +20,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. @@ -34,7 +34,8 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, layout: SwiftUISnapshotLayout = .sizeThatFits, - traits: UITraitCollection = .init() + traits: UITraitCollection = .init(), + format: ImageFormat ) -> Snapshotting { let config: ViewImageConfig @@ -51,7 +52,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { config = .init(safeArea: .zero, size: size, traits: traits) } - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale, format: format).asyncPullback { view in var config = config let controller: UIViewController diff --git a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift index 826044f4f..4a8287c31 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift @@ -4,14 +4,14 @@ import UIKit extension Snapshotting where Value == UIBezierPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing bezier paths based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. - public static func image(precision: Float = 1, scale: CGFloat = 1) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in + public static func image(precision: Float = 1, scale: CGFloat = 1, format: ImageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, scale: scale, format: format).pullback { path in let bounds = path.bounds let format: UIGraphicsImageRendererFormat if #available(iOS 11.0, tvOS 11.0, *) { diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index b5ff68048..af2c3bb84 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -4,14 +4,14 @@ import XCTest extension Diffing where Value == UIImage { /// A pixel-diffing strategy for UIImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1, scale: nil) + public static let image = Diffing.image(precision: 1, scale: nil, format: .defaultValue) /// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be. /// /// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels. /// - Parameter scale: Scale to use when loading the reference image from disk. If `nil` or the `UITraitCollection`s default value of `0.0`, the screens scale is used. /// - Returns: A new diffing strategy. - public static func image(precision: Float, scale: CGFloat?) -> Diffing { + public static func image(precision: Float, scale: CGFloat?, format: ImageFormat) -> Diffing { let imageScale: CGFloat if let scale = scale, scale != 0.0 { imageScale = scale @@ -20,10 +20,10 @@ extension Diffing where Value == UIImage { } return Diffing( - toData: { $0.pngData() ?? emptyImage().pngData()! }, - fromData: { UIImage(data: $0, scale: imageScale)! } + toData: { EncodeImage(image: $0, format)! }, + fromData: { DecodeImage(data: $0, format)! } ) { old, new in - guard !compare(old, new, precision: precision) else { return nil } + guard !compare(old, new, precision: precision, format: format) else { return nil } let difference = SnapshotTesting.diff(old, new) let message = new.size == old.size ? "Newly-taken snapshot does not match reference." @@ -56,22 +56,22 @@ extension Diffing where Value == UIImage { extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, scale: nil) + return .image(precision: 1, scale: nil, format: .defaultValue) } /// A snapshot strategy for comparing images based on pixel equality. /// /// - Parameter precision: The percentage of pixels that must match. /// - Parameter scale: The scale of the reference image stored on disk. - public static func image(precision: Float, scale: CGFloat?) -> Snapshotting { + public static func image(precision: Float, scale: CGFloat?, format: ImageFormat) -> Snapshotting { return .init( - pathExtension: "png", - diffing: .image(precision: precision, scale: scale) + pathExtension: format.rawValue, + diffing: .image(precision: precision, scale: scale, format: format) ) } } -private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool { +private func compare(_ old: UIImage, _ new: UIImage, precision: Float, format: ImageFormat) -> Bool { guard let oldCgImage = old.cgImage else { return false } guard let newCgImage = new.cgImage else { return false } guard oldCgImage.width != 0 else { return false } @@ -93,7 +93,7 @@ private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool { if let newContext = context(for: newCgImage, bytesPerRow: minBytesPerRow), let newData = newContext.data { if memcmp(oldData, newData, byteCount) == 0 { return true } } - let newer = UIImage(data: new.pngData()!)! + let newer = UIImage(data: EncodeImage(image: new, format)!)! guard let newerCgImage = newer.cgImage else { return false } var newerBytes = [UInt8](repeating: 0, count: byteCount) guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false } diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index fe1e81a5a..d5b7d938e 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -4,7 +4,7 @@ import UIKit extension Snapshotting where Value == UIView, Format == UIImage { /// A snapshot strategy for comparing views based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing views based on pixel equality. @@ -18,11 +18,12 @@ extension Snapshotting where Value == UIView, Format == UIImage { drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, size: CGSize? = nil, - traits: UITraitCollection = .init() - ) + traits: UITraitCollection = .init(), + format: ImageFormat + ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale, format: format).asyncPullback { view in snapshotView( config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index 45e719cbf..06c0ebea8 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -4,7 +4,7 @@ import UIKit extension Snapshotting where Value == UIViewController, Format == UIImage { /// A snapshot strategy for comparing view controller views based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: .defaultValue) } /// A snapshot strategy for comparing view controller views based on pixel equality. @@ -18,11 +18,12 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { on config: ViewImageConfig, precision: Float = 1, size: CGSize? = nil, - traits: UITraitCollection = .init() - ) + traits: UITraitCollection = .init(), + format: ImageFormat + ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale, format: format).asyncPullback { viewController in snapshotView( config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) } ?? config, drawHierarchyInKeyWindow: false, @@ -44,11 +45,12 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { drawHierarchyInKeyWindow: Bool = false, precision: Float = 1, size: CGSize? = nil, - traits: UITraitCollection = .init() - ) + traits: UITraitCollection = .init(), + format: ImageFormat + ) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in + return SimplySnapshotting.image(precision: precision, scale: traits.displayScale, format: format).asyncPullback { viewController in snapshotView( config: .init(safeArea: .zero, size: size, traits: traits), drawHierarchyInKeyWindow: drawHierarchyInKeyWindow, From d3abda006dc4649a94f1321698b148e9df8f9c67 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Wed, 4 Sep 2024 23:45:59 +0200 Subject: [PATCH 04/12] feat: make test compile --- Sources/SnapshotTesting/Snapshotting/NSView.swift | 2 +- Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift | 2 +- Sources/SnapshotTesting/Snapshotting/UIView.swift | 2 +- Sources/SnapshotTesting/Snapshotting/UIViewController.swift | 2 +- Tests/SnapshotTestingTests/SnapshotTestingTests.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/NSView.swift b/Sources/SnapshotTesting/Snapshotting/NSView.swift index 7fcfa1dbd..3b162ee67 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSView.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSView.swift @@ -12,7 +12,7 @@ extension Snapshotting where Value == NSView, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat) -> Snapshotting { + public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat = .defaultValue) -> Snapshotting { return SimplySnapshotting.image(precision: precision, format: .defaultValue).asyncPullback { view in let initialSize = view.frame.size if let size = size { view.frame.size = size } diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index 711e17624..c09e313c3 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -35,7 +35,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { precision: Float = 1, layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init(), - format: ImageFormat + format: ImageFormat = .defaultValue ) -> Snapshotting { let config: ViewImageConfig diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index d5b7d938e..67dc110c0 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -19,7 +19,7 @@ extension Snapshotting where Value == UIView, Format == UIImage { precision: Float = 1, size: CGSize? = nil, traits: UITraitCollection = .init(), - format: ImageFormat + format: ImageFormat = .defaultValue ) -> Snapshotting { diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index 06c0ebea8..a436fbffa 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -19,7 +19,7 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { precision: Float = 1, size: CGSize? = nil, traits: UITraitCollection = .init(), - format: ImageFormat + format: ImageFormat = .defaultValue ) -> Snapshotting { diff --git a/Tests/SnapshotTestingTests/SnapshotTestingTests.swift b/Tests/SnapshotTestingTests/SnapshotTestingTests.swift index e1aae6212..efd8eb1db 100644 --- a/Tests/SnapshotTestingTests/SnapshotTestingTests.swift +++ b/Tests/SnapshotTestingTests/SnapshotTestingTests.swift @@ -27,7 +27,7 @@ final class SnapshotTestingTests: XCTestCase { } override func tearDown() { - record = false +// record = false super.tearDown() } From 006c91f85208bb8034578c2697c037fd8c3649f8 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Thu, 5 Sep 2024 00:15:40 +0200 Subject: [PATCH 05/12] feat: use global imageFormat --- Sources/SnapshotTesting/AssertSnapshot.swift | 3 +++ Sources/SnapshotTesting/Snapshotting/CALayer.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/CGPath.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift | 2 +- Sources/SnapshotTesting/Snapshotting/NSImage.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/NSView.swift | 6 +++--- Sources/SnapshotTesting/Snapshotting/NSViewController.swift | 2 +- Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift | 2 +- Sources/SnapshotTesting/Snapshotting/UIImage.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/UIView.swift | 4 ++-- Sources/SnapshotTesting/Snapshotting/UIViewController.swift | 4 ++-- 12 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Sources/SnapshotTesting/AssertSnapshot.swift b/Sources/SnapshotTesting/AssertSnapshot.swift index 52b9a0919..ab69fb171 100644 --- a/Sources/SnapshotTesting/AssertSnapshot.swift +++ b/Sources/SnapshotTesting/AssertSnapshot.swift @@ -8,6 +8,9 @@ public var diffTool: String? = nil /// Whether or not to record all new references. public var record = false +/// 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: diff --git a/Sources/SnapshotTesting/Snapshotting/CALayer.swift b/Sources/SnapshotTesting/Snapshotting/CALayer.swift index 817f040e2..10b890c67 100644 --- a/Sources/SnapshotTesting/Snapshotting/CALayer.swift +++ b/Sources/SnapshotTesting/Snapshotting/CALayer.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == CALayer, Format == NSImage { /// A snapshot strategy for comparing layers based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, format: .defaultValue) + return .image(precision: 1, format: imageFormat) } /// A snapshot strategy for comparing layers based on pixel equality. @@ -29,7 +29,7 @@ import UIKit extension Snapshotting where Value == CALayer, Format == UIImage { /// A snapshot strategy for comparing layers based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing layers based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/CGPath.swift b/Sources/SnapshotTesting/Snapshotting/CGPath.swift index 7c44b7ea9..04679f6de 100644 --- a/Sources/SnapshotTesting/Snapshotting/CGPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/CGPath.swift @@ -4,7 +4,7 @@ import Cocoa 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(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing bezier paths based on pixel equality. @@ -33,7 +33,7 @@ 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(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing bezier paths based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift index 541ae3bfa..5528276cf 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSBezierPath.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == NSBezierPath, Format == NSImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing bezier paths based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index 433b88ab8..002eead1a 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -4,7 +4,7 @@ import XCTest extension Diffing where Value == NSImage { /// A pixel-diffing strategy for NSImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1, format: .defaultValue) + public static let image = Diffing.image(precision: 1, format: imageFormat) /// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be. /// @@ -31,7 +31,7 @@ extension Diffing where Value == NSImage { extension Snapshotting where Value == NSImage, Format == NSImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, format: .defaultValue) + return .image(precision: 1, format: imageFormat) } /// A snapshot strategy for comparing images based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/NSView.swift b/Sources/SnapshotTesting/Snapshotting/NSView.swift index 3b162ee67..deaf0815d 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSView.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSView.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == NSView, Format == NSImage { /// A snapshot strategy for comparing views based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing views based on pixel equality. @@ -12,8 +12,8 @@ extension Snapshotting where Value == NSView, Format == NSImage { /// - Parameters: /// - precision: The percentage of pixels that must match. /// - size: A view size override. - public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat = .defaultValue) -> Snapshotting { - return SimplySnapshotting.image(precision: precision, format: .defaultValue).asyncPullback { view in + public static func image(precision: Float = 1, size: CGSize? = nil, format: ImageFormat = imageFormat) -> Snapshotting { + return SimplySnapshotting.image(precision: precision, format: imageFormat).asyncPullback { view in let initialSize = view.frame.size if let size = size { view.frame.size = size } guard view.frame.width > 0, view.frame.height > 0 else { diff --git a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift index e8a5150a4..4409af671 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSViewController.swift @@ -4,7 +4,7 @@ import Cocoa extension Snapshotting where Value == NSViewController, Format == NSImage { /// A snapshot strategy for comparing view controller views based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing view controller views based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift index c09e313c3..401eb509a 100644 --- a/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/SwiftUIView.swift @@ -20,7 +20,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing SwiftUI Views based on pixel equality. @@ -35,7 +35,7 @@ extension Snapshotting where Value: SwiftUI.View, Format == UIImage { precision: Float = 1, layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init(), - format: ImageFormat = .defaultValue + format: ImageFormat = imageFormat ) -> Snapshotting { let config: ViewImageConfig diff --git a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift index 4a8287c31..dff7a5193 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIBezierPath.swift @@ -4,7 +4,7 @@ import UIKit extension Snapshotting where Value == UIBezierPath, Format == UIImage { /// A snapshot strategy for comparing bezier paths based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing bezier paths based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index af2c3bb84..4e94cc4b5 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -4,7 +4,7 @@ import XCTest extension Diffing where Value == UIImage { /// A pixel-diffing strategy for UIImage's which requires a 100% match. - public static let image = Diffing.image(precision: 1, scale: nil, format: .defaultValue) + public static let image = Diffing.image(precision: 1, scale: nil, format: imageFormat) /// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be. /// @@ -56,7 +56,7 @@ extension Diffing where Value == UIImage { extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image(precision: 1, scale: nil, format: .defaultValue) + return .image(precision: 1, scale: nil, format: imageFormat) } /// A snapshot strategy for comparing images based on pixel equality. diff --git a/Sources/SnapshotTesting/Snapshotting/UIView.swift b/Sources/SnapshotTesting/Snapshotting/UIView.swift index 67dc110c0..e46769ac3 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIView.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIView.swift @@ -4,7 +4,7 @@ import UIKit extension Snapshotting where Value == UIView, Format == UIImage { /// A snapshot strategy for comparing views based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing views based on pixel equality. @@ -19,7 +19,7 @@ extension Snapshotting where Value == UIView, Format == UIImage { precision: Float = 1, size: CGSize? = nil, traits: UITraitCollection = .init(), - format: ImageFormat = .defaultValue + format: ImageFormat = imageFormat ) -> Snapshotting { diff --git a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift index a436fbffa..b670c5966 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIViewController.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIViewController.swift @@ -4,7 +4,7 @@ import UIKit extension Snapshotting where Value == UIViewController, Format == UIImage { /// A snapshot strategy for comparing view controller views based on pixel equality. public static var image: Snapshotting { - return .image(format: .defaultValue) + return .image(format: imageFormat) } /// A snapshot strategy for comparing view controller views based on pixel equality. @@ -19,7 +19,7 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { precision: Float = 1, size: CGSize? = nil, traits: UITraitCollection = .init(), - format: ImageFormat = .defaultValue + format: ImageFormat = imageFormat ) -> Snapshotting { From be2149060ed48a071d07da945e9dc6f8a0d7126a Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Thu, 5 Sep 2024 01:08:44 +0200 Subject: [PATCH 06/12] fix: compilation issue$ --- Package.resolved | 12 +++++++++++- Package.swift | 7 ++----- Sources/SnapshotTesting/Snapshotting/NSImage.swift | 2 +- Sources/SnapshotTesting/Snapshotting/UIImage.swift | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Package.resolved b/Package.resolved index c3a7d4a7d..329026837 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,15 @@ { + "originHash" : "a16ed5d1f17dce3ee0b7cce7a04802759ee4b2faa99485261811c56f1adff67d", "pins" : [ + { + "identity" : "jxl-coder-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awxkee/jxl-coder-swift.git", + "state" : { + "revision" : "179264567c7dc0dd489859d5572773222358a7f5", + "version" : "1.7.3" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -10,5 +20,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index fd0c91631..5a37ef579 100644 --- a/Package.swift +++ b/Package.swift @@ -21,9 +21,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease") - ], - dependencies: [ + .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") ], targets: [ @@ -31,8 +29,7 @@ let package = Package( name: "SnapshotTesting", dependencies: [ .product(name: "JxlCoder", package: "jxl-coder-swift") - ]), - name: "SnapshotTesting" + ] ), .target( name: "InlineSnapshotTesting", diff --git a/Sources/SnapshotTesting/Snapshotting/NSImage.swift b/Sources/SnapshotTesting/Snapshotting/NSImage.swift index cf0463b72..0e12da0ac 100644 --- a/Sources/SnapshotTesting/Snapshotting/NSImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/NSImage.swift @@ -85,7 +85,7 @@ let byteCount = oldContext.height * oldContext.bytesPerRow if memcmp(oldData, newData, byteCount) == 0 { return nil } guard - let imageData = EncodeImage(image: new, format)!,, + let imageData = EncodeImage(image: new, format), let newerCgImage = NSImage(data: imageData)?.cgImage( forProposedRect: nil, context: nil, hints: nil), let newerContext = context(for: newerCgImage), diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 5355d1100..ea4cc253c 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -65,7 +65,7 @@ extension Snapshotting where Value == UIImage, Format == UIImage { /// A snapshot strategy for comparing images based on pixel equality. public static var image: Snapshotting { - return .image() + return .image(format: imageFormat) } /// A snapshot strategy for comparing images based on pixel equality. @@ -93,7 +93,7 @@ private let imageContextBitsPerComponent = 8 private let imageContextBytesPerPixel = 4 - private func compare(_ old: UIImage, _ new: UIImage, precision: Float, perceptualPrecision: Float) + private func compare(_ old: UIImage, _ new: UIImage, precision: Float, perceptualPrecision: Float, format: ImageFormat) -> String? { guard let oldCgImage = old.cgImage else { From 8f772fc79f1b350f40c15fce63d8f87d151c1740 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Thu, 5 Sep 2024 01:50:24 +0200 Subject: [PATCH 07/12] fix: cleaning --- .../SnapshotTesting/Snapshotting/ImageSerializer.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift index f9c18deef..58dc7b871 100644 --- a/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift +++ b/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift @@ -30,13 +30,7 @@ private func DecodePNGImage(_ data: Data) -> SnapImage? { #endif -package protocol DefaultValueProvider { - associatedtype Value - - static var defaultValue: Value { get } -} - -public enum ImageFormat: String, DefaultValueProvider { +public enum ImageFormat: String { case jxl case png From e53ff1e12427e605b244ff1141622f4724e0bec5 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Sat, 7 Sep 2024 23:28:12 +0200 Subject: [PATCH 08/12] fix: improve implementation add heic --- Package.swift | 14 ++++ Sources/ImageSerializer/ImageSerializer.swift | 53 ++++++++++++ Sources/InlineSnapshotTesting/Exports.swift | 1 + .../JPEGXLImageSerializer.swift | 26 ++++++ .../Snapshotting/HEICImageSerializer.swift | 80 +++++++++++++++++++ .../Snapshotting/ImageCoder.swift | 70 ++++++++++++++++ .../Snapshotting/ImageSerializer.swift | 53 ------------ .../Snapshotting/PNGImageSerializer.swift | 40 ++++++++++ 8 files changed, 284 insertions(+), 53 deletions(-) create mode 100644 Sources/ImageSerializer/ImageSerializer.swift create mode 100644 Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift create mode 100644 Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift create mode 100644 Sources/SnapshotTesting/Snapshotting/ImageCoder.swift delete mode 100644 Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift create mode 100644 Sources/SnapshotTesting/Snapshotting/PNGImageSerializer.swift diff --git a/Package.swift b/Package.swift index 5a37ef579..34589ce9b 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,10 @@ let package = Package( name: "SnapshotTesting", targets: ["SnapshotTesting"] ), + .library( + name: "JPEGXLImageSerializer", + targets: ["JPEGXLImageSerializer"] + ), .library( name: "InlineSnapshotTesting", targets: ["InlineSnapshotTesting"] @@ -28,9 +32,19 @@ let package = Package( .target( name: "SnapshotTesting", dependencies: [ + "ImageSerializer" + ] + ), + .target( + name: "JPEGXLImageSerializer", + dependencies: [ + "ImageSerializer", .product(name: "JxlCoder", package: "jxl-coder-swift") ] ), + .target( + name: "ImageSerializer" + ), .target( name: "InlineSnapshotTesting", dependencies: [ diff --git a/Sources/ImageSerializer/ImageSerializer.swift b/Sources/ImageSerializer/ImageSerializer.swift new file mode 100644 index 000000000..6cd1eb7db --- /dev/null +++ b/Sources/ImageSerializer/ImageSerializer.swift @@ -0,0 +1,53 @@ +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. +/// +/// The `defaultValue` is set to `.png`. +public enum ImageFormat: String { + case jxl + case png + case heic + + /// The default image format, set to `.png`. + public static var defaultValue = ImageFormat.png +} diff --git a/Sources/InlineSnapshotTesting/Exports.swift b/Sources/InlineSnapshotTesting/Exports.swift index 44c34dda7..d829d29ca 100644 --- a/Sources/InlineSnapshotTesting/Exports.swift +++ b/Sources/InlineSnapshotTesting/Exports.swift @@ -1 +1,2 @@ @_exported import SnapshotTesting +@_exported import ImageSerializer diff --git a/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift b/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift new file mode 100644 index 000000000..781282603 --- /dev/null +++ b/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift @@ -0,0 +1,26 @@ +import Foundation +import JxlCoder +import ImageSerializer + +// other package +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) + } + } +} diff --git a/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift new file mode 100644 index 000000000..6548e3774 --- /dev/null +++ b/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift @@ -0,0 +1,80 @@ +import Foundation +import ImageIO +import UniformTypeIdentifiers +import ImageSerializer + +#if canImport(UIKit) +import UIKit +#endif +#if canImport(AppKit) +import AppKit +#endif + +@available(iOS 14.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. +/// This functionality is available only on iOS 14.0 and later. +/// +/// - 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, *) +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 + ) + } +} diff --git a/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift new file mode 100644 index 000000000..817f882cb --- /dev/null +++ b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift @@ -0,0 +1,70 @@ +import Foundation +@_exported import ImageSerializer + +#if canImport(JPEGXLImageSerializer) +import JPEGXLImageSerializer +#endif + +/// Encodes an image into the specified format. +/// +/// This function takes a `SnapImage` and encodes it into a `Data` representation using the specified `ImageFormat`. +/// +/// - Parameters: +/// - image: The image to be encoded. This can be a `UIImage` on iOS or an `NSImage` on macOS. +/// - format: The format to encode the image into. Supported formats are `.png`, `.heic`, and `.jxl`. +/// +/// - Returns: The encoded image as `Data`, or `nil` if encoding fails. +/// +/// - Note: +/// - If the `.heic` format is selected and the platform does not support HEIC (iOS 14.0+), the image will be encoded as PNG. +/// - If the `.jxl` format is selected but `JPEGXLImageSerializer` is not available, the image will be encoded as PNG. +package func EncodeImage(image: SnapImage, _ format: ImageFormat) -> Data? { + var serializer: ImageSerializer + switch format { +#if canImport(JPEGXLImageSerializer) + case .jxl: serializer = ImageSerializer.jxl +#else + case .jxl: serializer = ImageSerializer.png +#endif + case .png: serializer = ImageSerializer.png + case .heic: + if #available(iOS 14.0, *) { + serializer = ImageSerializer.heic + } else { + serializer = ImageSerializer.png + } + } + return serializer.encodeImage(image) +} + +/// Decodes image data into a `SnapImage` of the specified format. +/// +/// This function takes `Data` representing an encoded image and decodes it back into a `SnapImage`. +/// +/// - Parameters: +/// - data: The data to be decoded into an image. +/// - format: The format of the image data. Supported formats are `.png`, `.heic`, and `.jxl`. +/// +/// - Returns: The decoded `SnapImage`, or `nil` if decoding fails. +/// +/// - Note: +/// - If the `.heic` format is selected and the platform does not support HEIC (iOS 14.0+), the image will be decoded as PNG. +/// - If the `.jxl` format is selected but `JPEGXLImageSerializer` is not available, the image will be decoded as PNG. +package func DecodeImage(data: Data, _ format: ImageFormat) -> SnapImage? { + var serializer: ImageSerializer + switch format { +#if canImport(JPEGXLImageSerializer) + case .jxl: serializer = ImageSerializer.jxl +#else + case .jxl: serializer = ImageSerializer.png +#endif + case .png: serializer = ImageSerializer.png + case .heic: + if #available(iOS 14.0, *) { + serializer = ImageSerializer.heic + } else { + serializer = ImageSerializer.png + } + } + return serializer.decodeImage(data) +} diff --git a/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift deleted file mode 100644 index 58dc7b871..000000000 --- a/Sources/SnapshotTesting/Snapshotting/ImageSerializer.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import JxlCoder - -#if !os(macOS) -import UIKit.UIImage -public typealias SnapImage = UIImage - -private func EncodePNGImage(_ image: SnapImage) -> Data? { - return image.pngData() -} - -private func DecodePNGImage(_ data: Data) -> SnapImage? { - UIImage(data: data) -} - -#else -import AppKit.NSImage -public typealias SnapImage = NSImage - -private func EncodePNGImage(_ image: SnapImage) -> Data? { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } - let rep = NSBitmapImageRep(cgImage: cgImage) - rep.size = image.size - return rep.representation(using: .png, properties: [:]) -} - -private func DecodePNGImage(_ data: Data) -> SnapImage? { - NSImage(data: data) -} - -#endif - -public enum ImageFormat: String { - case jxl - case png - - public static var defaultValue = ImageFormat.png -} - -package func EncodeImage(image: SnapImage, _ format: ImageFormat) -> Data? { - switch format { - case .jxl: return try? JXLCoder.encode(image: image) - case .png: return EncodePNGImage(image) - } -} - -package func DecodeImage(data: Data, _ format: ImageFormat) -> SnapImage? { - switch format { - case .jxl: return try? JXLCoder.decode(data: data) - case .png: return DecodePNGImage(data) - } -} - diff --git a/Sources/SnapshotTesting/Snapshotting/PNGImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/PNGImageSerializer.swift new file mode 100644 index 000000000..c0669dab1 --- /dev/null +++ b/Sources/SnapshotTesting/Snapshotting/PNGImageSerializer.swift @@ -0,0 +1,40 @@ +import Foundation +import ImageSerializer + +#if canImport(UIKit) +import UIKit +#endif +#if canImport(AppKit) +import AppKit +#endif + +struct PNGCoder { + static func encodeImage(_ image: SnapImage) -> Data? { +#if !os(macOS) + return image.pngData() +#else + guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + let bitmapRep = NSBitmapImageRep(cgImage: cgImage) + return bitmapRep.representation(using: .png, properties: [:]) +#endif + } + + static func decodeImage(_ data: Data) -> SnapImage? { +#if !os(macOS) + return UIImage(data: data) +#else + return NSImage(data: data) +#endif + } +} + +extension ImageSerializer { + package static var png: Self { + ImageSerializer( + encodeImage: PNGCoder.encodeImage, + decodeImage: PNGCoder.decodeImage + ) + } +} From 42bddc45b36fd07ed669d380e92a148544bfa651 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Sat, 7 Sep 2024 23:44:26 +0200 Subject: [PATCH 09/12] fix: cleanup a bit --- .../Snapshotting/HEICImageSerializer.swift | 98 +++++++++---------- .../Snapshotting/ImageCoder.swift | 4 +- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift b/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift index 6548e3774..69bb8be10 100644 --- a/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift +++ b/Sources/SnapshotTesting/Snapshotting/HEICImageSerializer.swift @@ -10,71 +10,67 @@ import UIKit import AppKit #endif -@available(iOS 14.0, *) +@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. -/// This functionality is available only on iOS 14.0 and later. /// /// - 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? { + /// 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 } + guard let cgImage = image.cgImage else { return nil } #else - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } + 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? { + 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) + return UIImage(data: data) #else - return NSImage(data: data) + return NSImage(data: data) #endif - } + } } -@available(iOS 14.0, *) +@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 - ) - } + /// 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 + ) + } } diff --git a/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift index 817f882cb..230e6a4dd 100644 --- a/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift +++ b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift @@ -28,7 +28,7 @@ package func EncodeImage(image: SnapImage, _ format: ImageFormat) -> Data? { #endif case .png: serializer = ImageSerializer.png case .heic: - if #available(iOS 14.0, *) { + if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { serializer = ImageSerializer.heic } else { serializer = ImageSerializer.png @@ -60,7 +60,7 @@ package func DecodeImage(data: Data, _ format: ImageFormat) -> SnapImage? { #endif case .png: serializer = ImageSerializer.png case .heic: - if #available(iOS 14.0, *) { + if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { serializer = ImageSerializer.heic } else { serializer = ImageSerializer.png From ef1e6e0a713fa40529c8cba47ce8cb29500f195b Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Sat, 7 Sep 2024 23:50:02 +0200 Subject: [PATCH 10/12] fix: cleanup a bit --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 34589ce9b..d76d7db44 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,9 @@ let package = Package( "ImageSerializer" ] ), + .target( + name: "ImageSerializer" + ), .target( name: "JPEGXLImageSerializer", dependencies: [ @@ -42,9 +45,6 @@ let package = Package( .product(name: "JxlCoder", package: "jxl-coder-swift") ] ), - .target( - name: "ImageSerializer" - ), .target( name: "InlineSnapshotTesting", dependencies: [ From f6647b5aefdf2934ecc43c1615a70b396957a7cb Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Sun, 8 Sep 2024 00:06:50 +0200 Subject: [PATCH 11/12] feat: add support for webp --- Package.resolved | 20 +++++++++- Package.swift | 14 ++++++- Sources/ImageSerializer/ImageSerializer.swift | 4 +- .../JPEGXLImageSerializer.swift | 37 +++++++++---------- .../Snapshotting/ImageCoder.swift | 21 ++++++++++- .../WEBPImageSerialize.swift | 26 +++++++++++++ 6 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 Sources/WEBPImageSerializer/WEBPImageSerialize.swift diff --git a/Package.resolved b/Package.resolved index 329026837..c60f57425 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a16ed5d1f17dce3ee0b7cce7a04802759ee4b2faa99485261811c56f1adff67d", + "originHash" : "de9ffd5f38c1f245e5a3028d1c9ff43af00171e62802d1b0dfcb28c3486c7c54", "pins" : [ { "identity" : "jxl-coder-swift", @@ -10,6 +10,15 @@ "version" : "1.7.3" } }, + { + "identity" : "libwebp-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awxkee/libwebp-ios.git", + "state" : { + "revision" : "09ab26afc64c55a49332c396c4b465c278fcb05f", + "version" : "1.1.1" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -18,6 +27,15 @@ "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", "version" : "600.0.0-prerelease-2024-06-12" } + }, + { + "identity" : "webp.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awxkee/webp.swift.git", + "state" : { + "revision" : "5ee6b41965c161b3adbe29f6c01a2b66b4944867", + "version" : "1.1.1" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index d76d7db44..a96a8610a 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,10 @@ let package = Package( name: "JPEGXLImageSerializer", targets: ["JPEGXLImageSerializer"] ), + .library( + name: "WEBPImageSerializer", + targets: ["WEBPImageSerializer"] + ), .library( name: "InlineSnapshotTesting", targets: ["InlineSnapshotTesting"] @@ -26,7 +30,8 @@ let package = Package( ], dependencies: [ .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/jxl-coder-swift.git", from: "1.7.3"), + .package(url: "https://github.com/awxkee/webp.swift.git", from: "1.1.1"), ], targets: [ .target( @@ -45,6 +50,13 @@ let package = Package( .product(name: "JxlCoder", package: "jxl-coder-swift") ] ), + .target( + name: "WEBPImageSerializer", + dependencies: [ + "ImageSerializer", + .product(name: "webp", package: "webp.swift") + ] + ), .target( name: "InlineSnapshotTesting", dependencies: [ diff --git a/Sources/ImageSerializer/ImageSerializer.swift b/Sources/ImageSerializer/ImageSerializer.swift index 6cd1eb7db..5f95fdbe4 100644 --- a/Sources/ImageSerializer/ImageSerializer.swift +++ b/Sources/ImageSerializer/ImageSerializer.swift @@ -41,13 +41,15 @@ package struct ImageSerializer { /// - `.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 + /// The default image format, set to `.png`. public static var defaultValue = ImageFormat.png } diff --git a/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift b/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift index 781282603..6882f634d 100644 --- a/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift +++ b/Sources/JPEGXLImageSerializer/JPEGXLImageSerializer.swift @@ -2,25 +2,24 @@ import Foundation import JxlCoder import ImageSerializer -// other package 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) - } + /// 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) } + } } diff --git a/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift index 230e6a4dd..d29ce5bac 100644 --- a/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift +++ b/Sources/SnapshotTesting/Snapshotting/ImageCoder.swift @@ -5,6 +5,10 @@ import Foundation import JPEGXLImageSerializer #endif +#if canImport(WEBPImageSerializer) +import WEBPImageSerializer +#endif + /// Encodes an image into the specified format. /// /// This function takes a `SnapImage` and encodes it into a `Data` representation using the specified `ImageFormat`. @@ -27,11 +31,17 @@ package func EncodeImage(image: SnapImage, _ format: ImageFormat) -> Data? { case .jxl: serializer = ImageSerializer.png #endif case .png: serializer = ImageSerializer.png + case .webp: + serializer = ImageSerializer.png +#if canImport(WEBPImageSerializer) + if #available(iOS 13.0, macOS 10.10, tvOS 13.0, watchOS 6.0, *) { + serializer = ImageSerializer.webp + } +#endif case .heic: + serializer = ImageSerializer.png if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { serializer = ImageSerializer.heic - } else { - serializer = ImageSerializer.png } } return serializer.encodeImage(image) @@ -59,6 +69,13 @@ package func DecodeImage(data: Data, _ format: ImageFormat) -> SnapImage? { case .jxl: serializer = ImageSerializer.png #endif case .png: serializer = ImageSerializer.png + case .webp: + serializer = ImageSerializer.png +#if canImport(WEBPImageSerializer) + if #available(iOS 13.0, macOS 10.10, tvOS 13.0, watchOS 6.0, *) { + serializer = ImageSerializer.webp + } +#endif case .heic: if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { serializer = ImageSerializer.heic diff --git a/Sources/WEBPImageSerializer/WEBPImageSerialize.swift b/Sources/WEBPImageSerializer/WEBPImageSerialize.swift new file mode 100644 index 000000000..30358240a --- /dev/null +++ b/Sources/WEBPImageSerializer/WEBPImageSerialize.swift @@ -0,0 +1,26 @@ +import Foundation +import webp +import ImageSerializer + +extension ImageSerializer { + /// A static property that provides an `ImageSerializer` for the WebP format. + /// + /// This property uses the `WebPEncoder` and `WebPDecoder` to encode and decode images in the WebP format. + /// + /// - Returns: An `ImageSerializer` instance configured for encoding and decoding WebP images. + /// + /// - Encoding: + /// - The `encodeImage` closure uses `WebPEncoder.encode(_:config:)` to convert a `SnapImage` into `Data` with the specified encoding configuration. + /// - The configuration used is `.preset(.picture, quality: 80)`, which applies a preset for general picture quality. + /// - Decoding: + /// - The `decodeImage` closure uses `WebPDecoder.decode(toImage:options:)` to convert `Data` back into a `SnapImage` with specified decoding options. + /// + /// - Note: The encoding and decoding operations are performed using the `webp` library, which supports the WebP format. + package static var webp: Self { + return ImageSerializer { image in + try? WebPEncoder().encode(image, config: .preset(.picture, quality: 80)) + } decodeImage: { data in + try? WebPDecoder().decode(toImage: data, options: WebpDecoderOptions()) + } + } +} From 4b833719c9847c9b36226d0fdd5a8b4feeafc481 Mon Sep 17 00:00:00 2001 From: Jeffrey Macko Date: Sun, 8 Sep 2024 00:17:43 +0200 Subject: [PATCH 12/12] feat: cleaning --- Sources/ImageSerializer/ImageSerializer.swift | 1 - Sources/InlineSnapshotTesting/Exports.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/ImageSerializer/ImageSerializer.swift b/Sources/ImageSerializer/ImageSerializer.swift index 5f95fdbe4..1f2c9282e 100644 --- a/Sources/ImageSerializer/ImageSerializer.swift +++ b/Sources/ImageSerializer/ImageSerializer.swift @@ -50,6 +50,5 @@ public enum ImageFormat: String { case heic case webp - /// The default image format, set to `.png`. public static var defaultValue = ImageFormat.png } diff --git a/Sources/InlineSnapshotTesting/Exports.swift b/Sources/InlineSnapshotTesting/Exports.swift index d829d29ca..44c34dda7 100644 --- a/Sources/InlineSnapshotTesting/Exports.swift +++ b/Sources/InlineSnapshotTesting/Exports.swift @@ -1,2 +1 @@ @_exported import SnapshotTesting -@_exported import ImageSerializer