forked from pointfreeco/swift-snapshot-testing
/
UIImage.swift
130 lines (121 loc) · 5.72 KB
/
UIImage.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#if os(iOS) || os(tvOS)
import UIKit
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)
/// 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 allowedDifference: A value between 0 and 255, where 0 means color component values must match 100%.
/// - Returns: A new diffing strategy.
public static func image(precision: Float, allowedDifference: UInt8 = 0) -> Diffing {
return Diffing(
toData: { $0.pngData()! },
fromData: { UIImage(data: $0, scale: UIScreen.main.scale)! }
) { old, new in
guard !compare(old, new, precision: precision, allowedDifference: allowedDifference) else { return nil }
let difference = SnapshotTesting.diff(old, new)
let message = new.size == old.size
? "Newly-taken snapshot does not match reference."
: "Newly-taken snapshot@\(new.size) does not match reference@\(old.size)."
let oldAttachment = XCTAttachment(image: old)
oldAttachment.name = "reference"
let newAttachment = XCTAttachment(image: new)
newAttachment.name = "failure"
let differenceAttachment = XCTAttachment(image: difference)
differenceAttachment.name = "difference"
return (
message,
[oldAttachment, newAttachment, differenceAttachment]
)
}
}
}
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)
}
/// A snapshot strategy for comparing images based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
/// - Parameter allowedDifference: Allowed difference between color component values to assume them as match.
public static func image(precision: Float, allowedDifference: UInt8 = 0) -> Snapshotting {
return .init(
pathExtension: "png",
diffing: .image(precision: precision, allowedDifference: allowedDifference)
)
}
}
private func compare(_ old: UIImage, _ new: UIImage, precision: Float, allowedDifference: UInt8) -> Bool {
guard let oldCgImage = old.cgImage else { return false }
guard let newCgImage = new.cgImage else { return false }
guard oldCgImage.width != 0 else { return false }
guard newCgImage.width != 0 else { return false }
guard oldCgImage.width == newCgImage.width else { return false }
guard oldCgImage.height != 0 else { return false }
guard newCgImage.height != 0 else { return false }
guard oldCgImage.height == newCgImage.height else { return false }
// Values between images may differ due to padding to multiple of 64 bytes per row,
// because of that a freshly taken view snapshot may differ from one stored as PNG.
// At this point we're sure that size of both images is the same, so we can go with minimal `bytesPerRow` value
// and use it to create contexts.
let minBytesPerRow = min(oldCgImage.bytesPerRow, newCgImage.bytesPerRow)
let byteCount = minBytesPerRow * oldCgImage.height
var oldBytes = [UInt8](repeating: 0, count: byteCount)
guard let oldContext = context(for: oldCgImage, bytesPerRow: minBytesPerRow, data: &oldBytes) else { return false }
guard let oldData = oldContext.data else { return false }
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()!)!
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 }
guard let newerData = newerContext.data else { return false }
if memcmp(oldData, newerData, byteCount) == 0 { return true }
let precision = min(max(precision, 0), 1)
if precision == 1 && allowedDifference == 0 { return false }
if precision == 0 || allowedDifference == 255 { return true }
let threshold = 1 - precision
var differentPixelCount = 0
for byte in 0..<byteCount {
if (oldBytes[byte] != newerBytes[byte]) && (byte % 4 != 0) { // skip alpha testing because alpha is already premultiplied
if allowedDifference == 0 || abs(Int(oldBytes[byte]) - Int(newerBytes[byte])) > allowedDifference {
differentPixelCount += 1
if Float(differentPixelCount) / Float(byteCount) > threshold {
return false
}
}
}
}
return true
}
private func context(for cgImage: CGImage, bytesPerRow: Int, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
guard
let space = cgImage.colorSpace,
let context = CGContext(
data: data,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: bytesPerRow,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
else { return nil }
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
return context
}
private func diff(_ old: UIImage, _ new: UIImage) -> UIImage {
let width = max(old.size.width, new.size.width)
let height = max(old.size.height, new.size.height)
UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), true, 0)
new.draw(at: .zero)
old.draw(at: .zero, blendMode: .difference, alpha: 1)
let differenceImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return differenceImage
}
#endif