Skip to content

Commit

Permalink
Normalize image conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
lunij committed Jun 21, 2024
1 parent b165492 commit 8c6303b
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import CoreGraphics

enum ImageComparisonResult {
case cgContextDataConversionFailed
case cgImageConversionFailed
case isMatching
case isNotMatching
case perceptualComparisonFailed
Expand Down
108 changes: 67 additions & 41 deletions Sources/SnapshotTesting/Snapshotting/NSImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,36 @@
toData: convertToData,
fromData: { NSImage(data: $0)! }
) { old, new in
let result = compare(old, new, precision: precision, perceptualPrecision: perceptualPrecision)
switch result {
case .cgContextDataConversionFailed, .cgImageConversionFailed:
return ("Core Graphics failure", [])
case .isMatching:
return nil
case .isNotMatching:
let diff = diffImage(old, new)
return ("Snapshot does not match reference", attachments(old, new, diff))
case .perceptualComparisonFailed:
return ("Perceptual comparison failed", [])
case let .unequalSize(oldSize, newSize):
let diff = diffImage(old, new)
return ("Snapshot size \(newSize) is unequal to expected size \(oldSize)", attachments(old, new, diff))
case let .unmatchedPrecision(expectedPrecision, actualPrecision):
let diff = diffImage(old, new)
return ("Actual image precision \(actualPrecision) is less than expected \(expectedPrecision)", attachments(old, new, diff))
case let .unmatchedPrecisions(expectedPixelPrecision, actualPixelPrecision, expectedPerceptualPrecision, actualPerceptualPrecision):
let diff = diffImage(old, new)
return (
"""
The percentage of pixels that match \(actualPixelPrecision) is less than expected \(expectedPixelPrecision)
The lowest perceptual color precision \(actualPerceptualPrecision) is less than expected \(expectedPerceptualPrecision)
""",
attachments(old, new, diff)
)
do {
let result = try compare(old, new, precision: precision, perceptualPrecision: perceptualPrecision)
switch result {
case .cgContextDataConversionFailed:
return ("Core Graphics failure", [])
case .isMatching:
return nil
case .isNotMatching:
let diff = try diffImage(old, new)
return ("Snapshot does not match reference", attachments(old, new, diff))
case .perceptualComparisonFailed:
return ("Perceptual comparison failed", [])
case let .unequalSize(oldSize, newSize):
let diff = try diffImage(old, new)
return ("Snapshot size \(newSize) is unequal to expected size \(oldSize)", attachments(old, new, diff))
case let .unmatchedPrecision(expectedPrecision, actualPrecision):
let diff = try diffImage(old, new)
return ("Actual image precision \(actualPrecision) is less than expected \(expectedPrecision)", attachments(old, new, diff))
case let .unmatchedPrecisions(expectedPixelPrecision, actualPixelPrecision, expectedPerceptualPrecision, actualPerceptualPrecision):
let diff = try diffImage(old, new)
return (
"""
The percentage of pixels that match \(actualPixelPrecision) is less than expected \(expectedPixelPrecision)
The lowest perceptual color precision \(actualPerceptualPrecision) is less than expected \(expectedPerceptualPrecision)
""",
attachments(old, new, diff)
)
}
} catch {
return (error.localizedDescription, [])
}
}
}
Expand Down Expand Up @@ -83,9 +87,7 @@
if image.size.height == 0 {
throw ImageConversionError.zeroHeight
}
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
throw ImageConversionError.cgImageConversionFailed
}
let cgImage = try image.normalizedCGImage()
let rep = NSBitmapImageRep(cgImage: cgImage)
rep.size = image.size
guard let data = rep.representation(using: .png, properties: [:]) else {
Expand All @@ -94,13 +96,10 @@
return data
}

private func compare(_ old: NSImage, _ new: NSImage, precision: Float, perceptualPrecision: Float) -> ImageComparisonResult {
guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return .cgImageConversionFailed
}
guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return .cgImageConversionFailed
}
private func compare(_ old: NSImage, _ new: NSImage, precision: Float, perceptualPrecision: Float) throws -> ImageComparisonResult {
let oldCgImage = try old.normalizedCGImage()
let newCgImage = try new.normalizedCGImage()

guard oldCgImage.width == newCgImage.width, oldCgImage.height == newCgImage.height else {
return .unequalSize(old: oldCgImage.size, new: newCgImage.size)
}
Expand All @@ -116,13 +115,13 @@
}
guard
let data = try? convertToData(new),
let newerCgImage = NSImage(data: data)?.cgImage(forProposedRect: nil, context: nil, hints: nil),
let newerCgImage = try NSImage(data: data)?.normalizedCGImage(),
let newerContext = context(for: newerCgImage),
let newerData = newerContext.data
else {
return .cgContextDataConversionFailed
}
if memcmp(oldData, newerData, byteCount) == 0 {
if memcmp(oldData, newerData, byteCount) == 0 {
return .isMatching
}
if precision >= 1, perceptualPrecision >= 1 {
Expand Down Expand Up @@ -177,9 +176,9 @@
return context
}

private func diffImage(_ 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)!)
private func diffImage(_ old: NSImage, _ new: NSImage) throws -> NSImage {
let oldCiImage = CIImage(cgImage: try old.normalizedCGImage())
let newCiImage = CIImage(cgImage: try new.normalizedCGImage())
let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
differenceFilter.setValue(oldCiImage, forKey: kCIInputImageKey)
differenceFilter.setValue(newCiImage, forKey: kCIInputBackgroundImageKey)
Expand All @@ -192,4 +191,31 @@
difference.addRepresentation(rep)
return difference
}

private extension NSImage {
func normalizedCGImage() throws -> CGImage {
guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
throw ImageConversionError.cgImageConversionFailed
}

guard let context = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
throw ImageConversionError.cgImageConversionFailed
}

context.draw(cgImage, in: CGRect(origin: .zero, size: size))

guard let cgImage = context.makeImage() else {
throw ImageConversionError.cgImageConversionFailed
}
return cgImage
}
}
#endif
90 changes: 60 additions & 30 deletions Sources/SnapshotTesting/Snapshotting/UIImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,36 @@
toData: convertToData,
fromData: { UIImage(data: $0, scale: imageScale)! }
) { old, new in
let result = compare(old, new, precision: precision, perceptualPrecision: perceptualPrecision)
switch result {
case .cgContextDataConversionFailed, .cgImageConversionFailed:
return ("Core Graphics failure", [])
case .isMatching:
return nil
case .isNotMatching:
let diff = diffImage(old, new)
return ("Snapshot does not match reference", attachments(old, new, diff))
case .perceptualComparisonFailed:
return ("Perceptual comparison failed", [])
case let .unequalSize(oldSize, newSize):
let diff = diffImage(old, new)
return ("Snapshot size \(newSize) is unequal to expected size \(oldSize)", attachments(old, new, diff))
case let .unmatchedPrecision(expectedPrecision, actualPrecision):
let diff = diffImage(old, new)
return ("Actual image precision \(actualPrecision) is less than expected \(expectedPrecision)", attachments(old, new, diff))
case let .unmatchedPrecisions(expectedPixelPrecision, actualPixelPrecision, expectedPerceptualPrecision, actualPerceptualPrecision):
let diff = diffImage(old, new)
return (
"""
The percentage of pixels that match \(actualPixelPrecision) is less than expected \(expectedPixelPrecision)
The lowest perceptual color precision \(actualPerceptualPrecision) is less than expected \(expectedPerceptualPrecision)
""",
attachments(old, new, diff)
)
do {
let result = try compare(old, new, precision: precision, perceptualPrecision: perceptualPrecision)
switch result {
case .cgContextDataConversionFailed:
return ("Core Graphics failure", [])
case .isMatching:
return nil
case .isNotMatching:
let diff = diffImage(old, new)
return ("Snapshot does not match reference", attachments(old, new, diff))
case .perceptualComparisonFailed:
return ("Perceptual comparison failed", [])
case let .unequalSize(oldSize, newSize):
let diff = diffImage(old, new)
return ("Snapshot size \(newSize) is unequal to expected size \(oldSize)", attachments(old, new, diff))
case let .unmatchedPrecision(expectedPrecision, actualPrecision):
let diff = diffImage(old, new)
return ("Actual image precision \(actualPrecision) is less than expected \(expectedPrecision)", attachments(old, new, diff))
case let .unmatchedPrecisions(expectedPixelPrecision, actualPixelPrecision, expectedPerceptualPrecision, actualPerceptualPrecision):
let diff = diffImage(old, new)
return (
"""
The percentage of pixels that match \(actualPixelPrecision) is less than expected \(expectedPixelPrecision)
The lowest perceptual color precision \(actualPerceptualPrecision) is less than expected \(expectedPerceptualPrecision)
""",
attachments(old, new, diff)
)
}
} catch {
return (error.localizedDescription, [])
}
}
}
Expand Down Expand Up @@ -109,10 +113,9 @@
return data
}

private func compare(_ old: UIImage, _ new: UIImage, precision: Float, perceptualPrecision: Float) -> ImageComparisonResult {
guard let oldCgImage = old.cgImage, let newCgImage = new.cgImage else {
return .cgImageConversionFailed
}
private func compare(_ old: UIImage, _ new: UIImage, precision: Float, perceptualPrecision: Float) throws -> ImageComparisonResult {
let oldCgImage = try old.normalizedCGImage()
let newCgImage = try new.normalizedCGImage()
guard oldCgImage.width == newCgImage.width, oldCgImage.height == newCgImage.height else {
return .unequalSize(old: oldCgImage.size, new: newCgImage.size)
}
Expand Down Expand Up @@ -200,4 +203,31 @@
UIGraphicsEndImageContext()
return differenceImage
}

private extension UIImage {
func normalizedCGImage() throws -> CGImage {
guard let cgImage = cgImage else {
throw ImageConversionError.cgImageConversionFailed
}

guard let context = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
throw ImageConversionError.cgImageConversionFailed
}

context.draw(cgImage, in: CGRect(origin: .zero, size: size))

guard let cgImage = context.makeImage() else {
throw ImageConversionError.cgImageConversionFailed
}
return cgImage
}
}
#endif

0 comments on commit 8c6303b

Please sign in to comment.