Skip to content
This repository was archived by the owner on Jan 13, 2022. It is now read-only.

Commit 517bb74

Browse files
author
Chris Williams
committed
Report precent difference when using a tolerance
1 parent fbb2d27 commit 517bb74

File tree

5 files changed

+93
-85
lines changed

5 files changed

+93
-85
lines changed

FBSnapshotTestCase/Categories/UIImage+Compare.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232

3333
@interface UIImage (Compare)
3434

35+
/// Takes a tolerance percentage (0.0-1.0) and compares this image with another image. Returns YES if the images differ less than the tollerance.
3536
- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance;
3637

38+
/// Performs a bitmap comparison of this image to another image. Returns YES if the images are exactly the same.
39+
- (BOOL)fb_isEqualToImage:(UIImage *)image;
40+
41+
/// Returns the percent of total pixels that differ between this image and another image as a float ranging from 0.0 to 1.0.
42+
- (CGFloat)fb_differenceFromImage:(UIImage *)image;
43+
3744
@end

FBSnapshotTestCase/Categories/UIImage+Compare.m

Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -46,89 +46,64 @@ @implementation UIImage (Compare)
4646

4747
- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance
4848
{
49+
return [self fb_isEqualToImage:image] || (tolerance > 0.0 && [self fb_differenceFromImage:image] < tolerance);
50+
}
51+
52+
53+
- (BOOL)fb_isEqualToImage:(UIImage *)image {
4954
NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size.");
5055

51-
CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage));
52-
CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
53-
54-
// The images have the equal size, so we could use the smallest amount of bytes because of byte padding
55-
size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage));
56-
size_t referenceImageSizeBytes = referenceImageSize.height * minBytesPerRow;
57-
void *referenceImagePixels = calloc(1, referenceImageSizeBytes);
58-
void *imagePixels = calloc(1, referenceImageSizeBytes);
59-
60-
if (!referenceImagePixels || !imagePixels) {
61-
free(referenceImagePixels);
62-
free(imagePixels);
63-
return NO;
64-
}
56+
CGContextRef referenceContext = [self fb_bitmapContext];
57+
CGContextRef imageContext = [image fb_bitmapContext];
6558

66-
CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels,
67-
referenceImageSize.width,
68-
referenceImageSize.height,
69-
CGImageGetBitsPerComponent(self.CGImage),
70-
minBytesPerRow,
71-
CGImageGetColorSpace(self.CGImage),
72-
(CGBitmapInfo)kCGImageAlphaPremultipliedLast
73-
);
74-
CGContextRef imageContext = CGBitmapContextCreate(imagePixels,
75-
imageSize.width,
76-
imageSize.height,
77-
CGImageGetBitsPerComponent(image.CGImage),
78-
minBytesPerRow,
79-
CGImageGetColorSpace(image.CGImage),
80-
(CGBitmapInfo)kCGImageAlphaPremultipliedLast
81-
);
82-
83-
if (!referenceImageContext || !imageContext) {
84-
CGContextRelease(referenceImageContext);
85-
CGContextRelease(imageContext);
86-
free(referenceImagePixels);
87-
free(imagePixels);
88-
return NO;
89-
}
90-
91-
CGContextDrawImage(referenceImageContext, CGRectMake(0, 0, referenceImageSize.width, referenceImageSize.height), self.CGImage);
92-
CGContextDrawImage(imageContext, CGRectMake(0, 0, imageSize.width, imageSize.height), image.CGImage);
93-
94-
CGContextRelease(referenceImageContext);
59+
size_t pixelCount = CGBitmapContextGetHeight(referenceContext) * CGBitmapContextGetBytesPerRow(referenceContext);
60+
BOOL matches = (memcmp(CGBitmapContextGetData(referenceContext), CGBitmapContextGetData(imageContext), pixelCount) == 0);
61+
62+
CGContextRelease(referenceContext);
9563
CGContextRelease(imageContext);
64+
65+
return matches;
66+
}
9667

97-
BOOL imageEqual = YES;
98-
99-
// Do a fast compare if we can
100-
if (tolerance == 0) {
101-
imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0);
102-
} else {
103-
// Go through each pixel in turn and see if it is different
104-
const NSInteger pixelCount = referenceImageSize.width * referenceImageSize.height;
105-
106-
FBComparePixel *p1 = referenceImagePixels;
107-
FBComparePixel *p2 = imagePixels;
108-
109-
NSInteger numDiffPixels = 0;
110-
for (int n = 0; n < pixelCount; ++n) {
111-
// If this pixel is different, increment the pixel diff count and see
112-
// if we have hit our limit.
113-
if (p1->raw != p2->raw) {
114-
numDiffPixels ++;
115-
116-
CGFloat percent = (CGFloat)numDiffPixels / pixelCount;
117-
if (percent > tolerance) {
118-
imageEqual = NO;
119-
break;
120-
}
121-
}
68+
- (CGFloat)fb_differenceFromImage:(UIImage *)image {
69+
NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size.");
12270

123-
p1++;
124-
p2++;
71+
// Go through each pixel in turn and see if it is different
72+
CGContextRef referenceContext = [self fb_bitmapContext];
73+
CGContextRef imageContext = [image fb_bitmapContext];
74+
75+
FBComparePixel *p1 = CGBitmapContextGetData(referenceContext);
76+
FBComparePixel *p2 = CGBitmapContextGetData(imageContext);
77+
78+
NSInteger pixelCount = CGBitmapContextGetWidth(referenceContext) * CGBitmapContextGetHeight(referenceContext);
79+
NSInteger numDiffPixels = 0;
80+
for (int n = 0; n < pixelCount; ++n) {
81+
// If this pixel is different, increment the pixel diff count and see
82+
// if we have hit our limit.
83+
if (p1->raw != p2->raw) {
84+
numDiffPixels++;
12585
}
86+
87+
p1++;
88+
p2++;
12689
}
90+
91+
return (CGFloat)numDiffPixels / pixelCount;
92+
}
12793

128-
free(referenceImagePixels);
129-
free(imagePixels);
130-
131-
return imageEqual;
94+
- (CGContextRef)fb_bitmapContext {
95+
CGContextRef context = CGBitmapContextCreate(NULL,
96+
self.size.width,
97+
self.size.height,
98+
CGImageGetBitsPerComponent(self.CGImage),
99+
CGImageGetBytesPerRow(self.CGImage),
100+
CGImageGetColorSpace(self.CGImage),
101+
(CGBitmapInfo)kCGImageAlphaPremultipliedLast);
102+
103+
CGContextDrawImage(context, (CGRect){.size = self.size}, self.CGImage);
104+
105+
NSAssert(context != nil, @"Unable to create context for comparision.");
106+
return context;
132107
}
133108

134109
@end

FBSnapshotTestCase/FBSnapshotTestController.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ extern NSString *const FBCapturedImageKey;
4343
*/
4444
extern NSString *const FBDiffedImageKey;
4545

46+
/**
47+
Errors returned by the methods of FBSnapshotTestController will contain this key if a tolerance was specified.
48+
*/
49+
extern NSString *const FBPercentDifferenceKey;
50+
4651
/**
4752
Provides the heavy-lifting for FBSnapshotTestCase. It loads and saves images, along with performing the actual pixel-
4853
by-pixel comparison of images.

FBSnapshotTestCase/FBSnapshotTestController.m

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
NSString *const FBReferenceImageKey = @"FBReferenceImageKey";
2222
NSString *const FBCapturedImageKey = @"FBCapturedImageKey";
2323
NSString *const FBDiffedImageKey = @"FBDiffedImageKey";
24+
NSString *const FBPercentDifferenceKey = @"FBPercentDifferenceKey";
2425

2526
typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) {
2627
FBTestSnapshotFileNameTypeReference,
@@ -130,25 +131,44 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage
130131
error:(NSError **)errorPtr
131132
{
132133
BOOL sameImageDimensions = CGSizeEqualToSize(referenceImage.size, image.size);
133-
if (sameImageDimensions && [referenceImage fb_compareWithImage:image tolerance:tolerance]) {
134+
if (sameImageDimensions && [referenceImage fb_isEqualToImage:image]) {
134135
return YES;
135136
}
136137

138+
CGFloat percentDifference;
139+
if (tolerance > 0.0) {
140+
percentDifference = [referenceImage fb_differenceFromImage:image];
141+
if (percentDifference < tolerance) {
142+
return YES;
143+
}
144+
}
145+
137146
if (NULL != errorPtr) {
138147
NSString *errorDescription = sameImageDimensions ? @"Images different" : @"Images different sizes";
139-
NSString *errorReason = sameImageDimensions ? [NSString stringWithFormat:@"image pixels differed by more than %.2f%% from the reference image", tolerance * 100]
148+
NSString *errorReason = sameImageDimensions ? @"Images differed"
140149
: [NSString stringWithFormat:@"referenceImage:%@, image:%@", NSStringFromCGSize(referenceImage.size), NSStringFromCGSize(image.size)];
141150
FBSnapshotTestControllerErrorCode errorCode = sameImageDimensions ? FBSnapshotTestControllerErrorCodeImagesDifferent : FBSnapshotTestControllerErrorCodeImagesDifferentSizes;
142151

143-
*errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
144-
code:errorCode
145-
userInfo:@{
146-
NSLocalizedDescriptionKey: errorDescription,
147-
NSLocalizedFailureReasonErrorKey: errorReason,
148-
FBReferenceImageKey: referenceImage,
149-
FBCapturedImageKey: image,
150-
FBDiffedImageKey: [referenceImage fb_diffWithImage:image],
151-
}];
152+
if (sameImageDimensions && tolerance > 0.0) {
153+
*errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
154+
code:errorCode
155+
userInfo:@{
156+
NSLocalizedDescriptionKey: errorDescription,
157+
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Images differed by %.2f%% from the reference (with a tolerance of %.2f%%)", percentDifference * 100, tolerance * 100],
158+
FBReferenceImageKey: referenceImage,
159+
FBCapturedImageKey: image,
160+
FBPercentDifferenceKey: @(percentDifference * 100),
161+
}];
162+
} else {
163+
*errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain
164+
code:errorCode
165+
userInfo:@{
166+
NSLocalizedDescriptionKey: errorDescription,
167+
NSLocalizedFailureReasonErrorKey: errorReason,
168+
FBReferenceImageKey: referenceImage,
169+
FBCapturedImageKey: image,
170+
}];
171+
}
152172
}
153173
return NO;
154174
}

FBSnapshotTestCaseTests/FBSnapshotControllerTests.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldNotMatch
6060
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage tolerance:0.0001 error:&error]);
6161
XCTAssertNotNil(error);
6262
XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent);
63+
XCTAssertEqual([error.userInfo[FBPercentDifferenceKey] doubleValue], .04);
6364
}
6465

6566
- (void)testCompareReferenceImageWithVeryLowToleranceShouldMatch

0 commit comments

Comments
 (0)