diff --git a/Sources/SnapshotTesting/Snapshotting/UIImage.swift b/Sources/SnapshotTesting/Snapshotting/UIImage.swift index 3d1bb531..b2a2fe88 100644 --- a/Sources/SnapshotTesting/Snapshotting/UIImage.swift +++ b/Sources/SnapshotTesting/Snapshotting/UIImage.swift @@ -179,6 +179,11 @@ } private func diff(_ old: UIImage, _ new: UIImage) -> UIImage { + normalizedComponentDiff(old, new) + ?? blendModeDiff(old, new) + } + + private func blendModeDiff(_ old: UIImage, _ new: UIImage) -> UIImage { let width = max(old.size.width, new.size.width) let height = max(old.size.height, new.size.height) let scale = max(old.scale, new.scale) @@ -189,6 +194,97 @@ UIGraphicsEndImageContext() return differenceImage } + +private func normalizedComponentDiff(_ old: UIImage, _ new: UIImage) -> UIImage? { + guard let oldCgImage = old.cgImage, + let pngData = new.pngData(), + let newCgImage = UIImage(data: pngData)?.cgImage, + oldCgImage.width == newCgImage.width, + oldCgImage.height == newCgImage.height, + let oldData = oldCgImage.dataProvider?.data, + let newData = newCgImage.dataProvider?.data + else { + return nil + } + + guard let outputColorSpace = CGColorSpace(name: CGColorSpace.linearGray), + let outputFormat = vImage_CGImageFormat( + bitsPerComponent: imageContextBitsPerComponent, + bitsPerPixel: imageContextBitsPerComponent, + colorSpace: outputColorSpace, + bitmapInfo: .init(), + ) + else { + return nil + } + + let width = oldCgImage.width + let height = oldCgImage.height + let pixelCount = width * height + let scale = old.scale + + let oldBytes = CFDataGetBytePtr(oldData)! + let newBytes = CFDataGetBytePtr(newData)! + var diffBytes = [UInt8](repeating: 0, count: pixelCount) + + var index = 0 + while index < pixelCount { + defer { index += 1 } + let pixelOffset = index * imageContextBytesPerPixel + + let rOld = Int16(oldBytes[pixelOffset]) + let gOld = Int16(oldBytes[pixelOffset + 1]) + let bOld = Int16(oldBytes[pixelOffset + 2]) + let aOld = Int16(oldBytes[pixelOffset + 3]) + + let rNew = Int16(newBytes[pixelOffset]) + let gNew = Int16(newBytes[pixelOffset + 1]) + let bNew = Int16(newBytes[pixelOffset + 2]) + let aNew = Int16(newBytes[pixelOffset + 3]) + + let rDiff = abs(rOld - rNew) + let gDiff = abs(gOld - gNew) + let bDiff = abs(bOld - bNew) + let aDiff = abs(aOld - aNew) + + let maxDiff = max(rDiff, gDiff, bDiff, aDiff) + diffBytes[index] = UInt8(maxDiff) + } + + let outputCgImage: CGImage? = diffBytes.withUnsafeMutableBytes { diffPtr in + var diffBuffer = vImage_Buffer( + data: diffPtr.baseAddress, + height: vImagePixelCount(height), + width: vImagePixelCount(width), + rowBytes: width + ) + + do { + var normalizedBuffer = try vImage_Buffer( + width: width, + height: height, + bitsPerPixel: UInt32(imageContextBitsPerComponent) + ) + defer { normalizedBuffer.free() } + + let error = vImageContrastStretch_Planar8( + &diffBuffer, + &normalizedBuffer, + vImage_Flags(kvImageNoFlags) + ) + + let buffer = error == kvImageNoError ? normalizedBuffer : diffBuffer + + return try buffer.createCGImage(format: outputFormat) + } catch { + return nil + } + } + + guard let outputCgImage else { return nil } + + return UIImage(cgImage: outputCgImage, scale: scale, orientation: .up) +} #endif #if os(iOS) || os(tvOS) || os(macOS)