Skip to content

Commit cfdfdd4

Browse files
authored
Merge pull request #2 from alexey1312/feature/addMacOSPlatform
add mac os platform
2 parents a6228e5 + 1d56cb7 commit cfdfdd4

File tree

7 files changed

+242
-4
lines changed

7 files changed

+242
-4
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let package = Package(
88
name: "SnapshotTestingHEIC",
99
platforms: [
1010
.iOS(.v11),
11-
.macOS(.v10_10),
11+
.macOS(.v10_13),
1212
.tvOS(.v10)
1313
],
1414
products: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#if os(macOS)
2+
import AVFoundation
3+
import Cocoa
4+
5+
extension NSImage {
6+
func heicData(compressionQuality: CompressionQuality = .lossless) -> Data? {
7+
let data = NSMutableData()
8+
9+
guard let imageDestination = CGImageDestinationCreateWithData(
10+
data, AVFileType.heic as CFString, 1, nil
11+
)
12+
else { return nil }
13+
14+
guard let cgImage = cgImage(forProposedRect: nil,
15+
context: nil,
16+
hints: nil)
17+
else { return nil }
18+
19+
let options: NSDictionary = [
20+
kCGImageDestinationLossyCompressionQuality: compressionQuality.value
21+
]
22+
23+
CGImageDestinationAddImage(imageDestination, cgImage, options)
24+
25+
guard CGImageDestinationFinalize(imageDestination) else { return nil }
26+
27+
return data as Data
28+
}
29+
}
30+
#endif
31+
+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#if os(macOS)
2+
import Cocoa
3+
import XCTest
4+
@testable import SnapshotTesting
5+
6+
public extension Diffing where Value == NSImage {
7+
/// A pixel-diffing strategy for NSImage's which requires a 100% match.
8+
static let imageHEIC = Diffing.imageHEIC(precision: 1, compressionQuality: .lossless)
9+
10+
/// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be.
11+
///
12+
/// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels.
13+
/// - Returns: A new diffing strategy.
14+
static func imageHEIC(precision: Float, compressionQuality: CompressionQuality = .lossless) -> Diffing {
15+
return .init(
16+
toData: { NSImageHEICRepresentation($0, compressionQuality: compressionQuality)! },
17+
fromData: { NSImage(data: $0)! }
18+
) { old, new in
19+
guard !compare(old, new, precision: precision, compressionQuality: compressionQuality)
20+
else { return nil }
21+
let difference = diffNSImage(old, new)
22+
let message = new.size == old.size
23+
? "Newly-taken snapshot does not match reference."
24+
: "Newly-taken snapshot@\(new.size) does not match reference@\(old.size)."
25+
return (
26+
message,
27+
[XCTAttachment(image: old), XCTAttachment(image: new), XCTAttachment(image: difference)]
28+
)
29+
}
30+
}
31+
}
32+
33+
public extension Snapshotting where Value == NSImage, Format == NSImage {
34+
/// A snapshot strategy for comparing images based on pixel equality.
35+
static var imageHEIC: Snapshotting {
36+
return .imageHEIC(precision: 1)
37+
}
38+
39+
/// A snapshot strategy for comparing images based on pixel equality.
40+
///
41+
/// - Parameter precision: The percentage of pixels that must match.
42+
static func imageHEIC(precision: Float) -> Snapshotting {
43+
return .init(pathExtension: "heic", diffing: .imageHEIC(precision: precision))
44+
}
45+
}
46+
47+
private func NSImageHEICRepresentation(_ image: NSImage, compressionQuality: CompressionQuality) -> Data? {
48+
return image.heicData(compressionQuality: compressionQuality)
49+
}
50+
51+
private func compare(
52+
_ old: NSImage,
53+
_ new: NSImage,
54+
precision: Float,
55+
compressionQuality: CompressionQuality
56+
) -> Bool {
57+
guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
58+
guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
59+
guard oldCgImage.width != 0 else { return false }
60+
guard newCgImage.width != 0 else { return false }
61+
guard oldCgImage.width == newCgImage.width else { return false }
62+
guard oldCgImage.height != 0 else { return false }
63+
guard newCgImage.height != 0 else { return false }
64+
guard oldCgImage.height == newCgImage.height else { return false }
65+
guard let oldContext = context(for: oldCgImage) else { return false }
66+
guard let newContext = context(for: newCgImage) else { return false }
67+
guard let oldData = oldContext.data else { return false }
68+
guard let newData = newContext.data else { return false }
69+
let byteCount = oldContext.height * oldContext.bytesPerRow
70+
if memcmp(oldData, newData, byteCount) == 0 { return true }
71+
let newer = NSImage(data: NSImageHEICRepresentation(new, compressionQuality: compressionQuality)!)!
72+
guard let newerCgImage = newer.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
73+
guard let newerContext = context(for: newerCgImage) else { return false }
74+
guard let newerData = newerContext.data else { return false }
75+
if memcmp(oldData, newerData, byteCount) == 0 { return true }
76+
if precision >= 1 { return false }
77+
let oldRep = NSBitmapImageRep(cgImage: oldCgImage)
78+
let newRep = NSBitmapImageRep(cgImage: newerCgImage)
79+
var differentPixelCount = 0
80+
let pixelCount = oldRep.pixelsWide * oldRep.pixelsHigh
81+
let threshold = (1 - precision) * Float(pixelCount)
82+
let p1: UnsafeMutablePointer<UInt8> = oldRep.bitmapData!
83+
let p2: UnsafeMutablePointer<UInt8> = newRep.bitmapData!
84+
for offset in 0 ..< pixelCount * 4 {
85+
if p1[offset] != p2[offset] {
86+
differentPixelCount += 1
87+
}
88+
if Float(differentPixelCount) > threshold { return false }
89+
}
90+
return true
91+
}
92+
93+
private func context(for cgImage: CGImage) -> CGContext? {
94+
guard
95+
let space = cgImage.colorSpace,
96+
let context = CGContext(
97+
data: nil,
98+
width: cgImage.width,
99+
height: cgImage.height,
100+
bitsPerComponent: cgImage.bitsPerComponent,
101+
bytesPerRow: cgImage.bytesPerRow,
102+
space: space,
103+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
104+
)
105+
else { return nil }
106+
107+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
108+
return context
109+
}
110+
111+
private func diffNSImage(_ old: NSImage, _ new: NSImage) -> NSImage {
112+
let oldCiImage = CIImage(cgImage: old.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
113+
let newCiImage = CIImage(cgImage: new.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
114+
let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
115+
differenceFilter.setValue(oldCiImage, forKey: kCIInputImageKey)
116+
differenceFilter.setValue(newCiImage, forKey: kCIInputBackgroundImageKey)
117+
let maxSize = CGSize(
118+
width: max(old.size.width, new.size.width),
119+
height: max(old.size.height, new.size.height)
120+
)
121+
let rep = NSCIImageRep(ciImage: differenceFilter.outputImage!)
122+
let difference = NSImage(size: maxSize)
123+
difference.addRepresentation(rep)
124+
return difference
125+
}
126+
#endif
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#if os(macOS)
2+
import Cocoa
3+
@testable import SnapshotTesting
4+
5+
public extension Snapshotting where Value == NSView, Format == NSImage {
6+
/// A snapshot strategy for comparing views based on pixel equality.
7+
static var imageHEIC: Snapshotting {
8+
return .imageHEIC()
9+
}
10+
11+
/// A snapshot strategy for comparing views based on pixel equality.
12+
///
13+
/// - Parameters:
14+
/// - precision: The percentage of pixels that must match.
15+
/// - size: A view size override.
16+
static func imageHEIC(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
17+
return SimplySnapshotting.imageHEIC(precision: precision).asyncPullback { view in
18+
let initialSize = view.frame.size
19+
if let size = size { view.frame.size = size }
20+
guard view.frame.width > 0, view.frame.height > 0 else {
21+
fatalError("View not renderable to image at size \(view.frame.size)")
22+
}
23+
return view.snapshot ?? Async { callback in
24+
addImagesForRenderedViews(view).sequence().run { views in
25+
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
26+
view.cacheDisplay(in: view.bounds, to: bitmapRep)
27+
let image = NSImage(size: view.bounds.size)
28+
image.addRepresentation(bitmapRep)
29+
callback(image)
30+
views.forEach { $0.removeFromSuperview() }
31+
view.frame.size = initialSize
32+
}
33+
}
34+
}
35+
}
36+
}
37+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#if os(macOS)
2+
import Cocoa
3+
@testable import SnapshotTesting
4+
5+
public extension Snapshotting where Value == NSViewController, Format == NSImage {
6+
/// A snapshot strategy for comparing view controller views based on pixel equality.
7+
static var imageHEIC: Snapshotting {
8+
return .imageHEIC()
9+
}
10+
11+
/// A snapshot strategy for comparing view controller views based on pixel equality.
12+
///
13+
/// - Parameters:
14+
/// - precision: The percentage of pixels that must match.
15+
/// - size: A view size override.
16+
static func imageHEIC(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
17+
return Snapshotting<NSView, NSImage>.imageHEIC(precision: precision, size: size).pullback { $0.view }
18+
}
19+
}
20+
#endif

Tests/SnapshotTestingHEICTests/SnapshotTestingHEICTests.swift

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
#if os(iOS) || os(tvOS)
21
import XCTest
32
import SnapshotTesting
43
@testable import SnapshotTestingHEIC
54

65
final class SnapshotTestingHEICTests: XCTestCase {
76

7+
#if os(iOS) || os(tvOS)
88
var sut: TestViewController!
99

1010
override func setUp() {
1111
super.setUp()
1212
sut = TestViewController()
13-
// isRecording = true
13+
// isRecording = true
1414
}
1515

1616
override func tearDown() {
17+
sut = nil
1718
super.tearDown()
1819
}
1920

@@ -40,6 +41,29 @@ final class SnapshotTestingHEICTests: XCTestCase {
4041
assertSnapshot(matching: sut, as: .imageHEIC(on: .iPadPro12_9,
4142
compressionQuality: 0.75))
4243
}
44+
#endif
4345

44-
}
46+
47+
#if os(macOS)
48+
func test_HEIC_NSView() {
49+
// given
50+
let view = NSView()
51+
let button = NSButton()
52+
// when
53+
view.frame = CGRect(origin: .zero, size: CGSize(width: 400, height: 400))
54+
view.wantsLayer = true
55+
view.layer?.backgroundColor = NSColor.blue.cgColor
56+
view.addSubview(button)
57+
button.frame.origin = CGPoint(x: view.frame.origin.x + view.frame.size.width / 2.0,
58+
y: view.frame.origin.y + view.frame.size.height / 2.0)
59+
button.bezelStyle = .rounded
60+
button.title = "Push Me"
61+
button.wantsLayer = true
62+
button.layer?.backgroundColor = NSColor.red.cgColor
63+
button.sizeToFit()
64+
// then
65+
assertSnapshot(matching: view, as: .imageHEIC)
66+
}
4567
#endif
68+
69+
}

0 commit comments

Comments
 (0)