Skip to content

Commit

Permalink
Merge pull request #2 from alexey1312/feature/addMacOSPlatform
Browse files Browse the repository at this point in the history
add mac os platform
  • Loading branch information
alexey1312 authored Apr 3, 2022
2 parents a6228e5 + 1d56cb7 commit cfdfdd4
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let package = Package(
name: "SnapshotTestingHEIC",
platforms: [
.iOS(.v11),
.macOS(.v10_10),
.macOS(.v10_13),
.tvOS(.v10)
],
products: [
Expand Down
31 changes: 31 additions & 0 deletions Sources/SnapshotTestingHEIC/HEIC/NSImage+HEIC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#if os(macOS)
import AVFoundation
import Cocoa

extension NSImage {
func heicData(compressionQuality: CompressionQuality = .lossless) -> Data? {
let data = NSMutableData()

guard let imageDestination = CGImageDestinationCreateWithData(
data, AVFileType.heic as CFString, 1, nil
)
else { return nil }

guard let cgImage = cgImage(forProposedRect: nil,
context: nil,
hints: nil)
else { return nil }

let options: NSDictionary = [
kCGImageDestinationLossyCompressionQuality: compressionQuality.value
]

CGImageDestinationAddImage(imageDestination, cgImage, options)

guard CGImageDestinationFinalize(imageDestination) else { return nil }

return data as Data
}
}
#endif

126 changes: 126 additions & 0 deletions Sources/SnapshotTestingHEIC/NSImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#if os(macOS)
import Cocoa
import XCTest
@testable import SnapshotTesting

public extension Diffing where Value == NSImage {
/// A pixel-diffing strategy for NSImage's which requires a 100% match.
static let imageHEIC = Diffing.imageHEIC(precision: 1, compressionQuality: .lossless)

/// 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.
static func imageHEIC(precision: Float, compressionQuality: CompressionQuality = .lossless) -> Diffing {
return .init(
toData: { NSImageHEICRepresentation($0, compressionQuality: compressionQuality)! },
fromData: { NSImage(data: $0)! }
) { old, new in
guard !compare(old, new, precision: precision, compressionQuality: compressionQuality)
else { return nil }
let difference = diffNSImage(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)."
return (
message,
[XCTAttachment(image: old), XCTAttachment(image: new), XCTAttachment(image: difference)]
)
}
}
}

public extension Snapshotting where Value == NSImage, Format == NSImage {
/// A snapshot strategy for comparing images based on pixel equality.
static var imageHEIC: Snapshotting {
return .imageHEIC(precision: 1)
}

/// A snapshot strategy for comparing images based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
static func imageHEIC(precision: Float) -> Snapshotting {
return .init(pathExtension: "heic", diffing: .imageHEIC(precision: precision))
}
}

private func NSImageHEICRepresentation(_ image: NSImage, compressionQuality: CompressionQuality) -> Data? {
return image.heicData(compressionQuality: compressionQuality)
}

private func compare(
_ old: NSImage,
_ new: NSImage,
precision: Float,
compressionQuality: CompressionQuality
) -> 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 }
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 }
guard let oldContext = context(for: oldCgImage) else { return false }
guard let newContext = context(for: newCgImage) else { return false }
guard let oldData = oldContext.data else { return false }
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: NSImageHEICRepresentation(new, compressionQuality: compressionQuality)!)!
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 }
if memcmp(oldData, newerData, byteCount) == 0 { return true }
if precision >= 1 { return false }
let oldRep = NSBitmapImageRep(cgImage: oldCgImage)
let newRep = NSBitmapImageRep(cgImage: newerCgImage)
var differentPixelCount = 0
let pixelCount = oldRep.pixelsWide * oldRep.pixelsHigh
let threshold = (1 - precision) * Float(pixelCount)
let p1: UnsafeMutablePointer<UInt8> = oldRep.bitmapData!
let p2: UnsafeMutablePointer<UInt8> = newRep.bitmapData!
for offset in 0 ..< pixelCount * 4 {
if p1[offset] != p2[offset] {
differentPixelCount += 1
}
if Float(differentPixelCount) > threshold { return false }
}
return true
}

private func context(for cgImage: CGImage) -> CGContext? {
guard
let space = cgImage.colorSpace,
let context = CGContext(
data: nil,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: cgImage.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 diffNSImage(_ old: NSImage, _ new: NSImage) -> NSImage {
let oldCiImage = CIImage(cgImage: old.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
let newCiImage = CIImage(cgImage: new.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
differenceFilter.setValue(oldCiImage, forKey: kCIInputImageKey)
differenceFilter.setValue(newCiImage, forKey: kCIInputBackgroundImageKey)
let maxSize = CGSize(
width: max(old.size.width, new.size.width),
height: max(old.size.height, new.size.height)
)
let rep = NSCIImageRep(ciImage: differenceFilter.outputImage!)
let difference = NSImage(size: maxSize)
difference.addRepresentation(rep)
return difference
}
#endif
37 changes: 37 additions & 0 deletions Sources/SnapshotTestingHEIC/NSView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#if os(macOS)
import Cocoa
@testable import SnapshotTesting

public extension Snapshotting where Value == NSView, Format == NSImage {
/// A snapshot strategy for comparing views based on pixel equality.
static var imageHEIC: Snapshotting {
return .imageHEIC()
}

/// A snapshot strategy for comparing views based on pixel equality.
///
/// - Parameters:
/// - precision: The percentage of pixels that must match.
/// - size: A view size override.
static func imageHEIC(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
return SimplySnapshotting.imageHEIC(precision: precision).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 {
fatalError("View not renderable to image at size \(view.frame.size)")
}
return view.snapshot ?? Async { callback in
addImagesForRenderedViews(view).sequence().run { views in
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
view.cacheDisplay(in: view.bounds, to: bitmapRep)
let image = NSImage(size: view.bounds.size)
image.addRepresentation(bitmapRep)
callback(image)
views.forEach { $0.removeFromSuperview() }
view.frame.size = initialSize
}
}
}
}
}
#endif
20 changes: 20 additions & 0 deletions Sources/SnapshotTestingHEIC/NSViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#if os(macOS)
import Cocoa
@testable import SnapshotTesting

public extension Snapshotting where Value == NSViewController, Format == NSImage {
/// A snapshot strategy for comparing view controller views based on pixel equality.
static var imageHEIC: Snapshotting {
return .imageHEIC()
}

/// A snapshot strategy for comparing view controller views based on pixel equality.
///
/// - Parameters:
/// - precision: The percentage of pixels that must match.
/// - size: A view size override.
static func imageHEIC(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
return Snapshotting<NSView, NSImage>.imageHEIC(precision: precision, size: size).pullback { $0.view }
}
}
#endif
30 changes: 27 additions & 3 deletions Tests/SnapshotTestingHEICTests/SnapshotTestingHEICTests.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
#if os(iOS) || os(tvOS)
import XCTest
import SnapshotTesting
@testable import SnapshotTestingHEIC

final class SnapshotTestingHEICTests: XCTestCase {

#if os(iOS) || os(tvOS)
var sut: TestViewController!

override func setUp() {
super.setUp()
sut = TestViewController()
// isRecording = true
// isRecording = true
}

override func tearDown() {
sut = nil
super.tearDown()
}

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

}

#if os(macOS)
func test_HEIC_NSView() {
// given
let view = NSView()
let button = NSButton()
// when
view.frame = CGRect(origin: .zero, size: CGSize(width: 400, height: 400))
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.blue.cgColor
view.addSubview(button)
button.frame.origin = CGPoint(x: view.frame.origin.x + view.frame.size.width / 2.0,
y: view.frame.origin.y + view.frame.size.height / 2.0)
button.bezelStyle = .rounded
button.title = "Push Me"
button.wantsLayer = true
button.layer?.backgroundColor = NSColor.red.cgColor
button.sizeToFit()
// then
assertSnapshot(matching: view, as: .imageHEIC)
}
#endif

}
Binary file not shown.

0 comments on commit cfdfdd4

Please sign in to comment.