Skip to content

Highlight different pixels #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FBSnapshotTestCase/Categories/UIImage+Compare.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
@param overallTolerance The overall percentage of pixels that are allowed to change from the pixels in the reference image.
@return A BOOL which represents if the image is the same or not.
*/
- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance;
- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance differentPixels:(NSMutableArray **)differentPixels;

@end

Expand Down
28 changes: 21 additions & 7 deletions FBSnapshotTestCase/Categories/UIImage+Compare.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

@implementation UIImage (Compare)

- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance
- (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance differentPixels:(NSMutableArray **)differentPixels
{
CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage));
CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));
Expand Down Expand Up @@ -96,7 +96,7 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel
FBComparePixel *p2 = imagePixels;

// Do a fast compare if we can
if (overallTolerance == 0 && perPixelTolerance == 0) {
if (overallTolerance == 0 && perPixelTolerance == 0 && !*differentPixels) {
imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0);
} else {
const NSUInteger pixelCount = referenceImageSize.width * referenceImageSize.height;
Expand All @@ -105,7 +105,8 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel
overallTolerance:overallTolerance
pixelCount:pixelCount
referencePixels:p1
imagePixels:p2];
imagePixels:p2
differentPixels:differentPixels];
}

free(referenceImagePixels);
Expand Down Expand Up @@ -155,24 +156,37 @@ - (BOOL)_compareAllPixelsWithPerPixelTolerance:(CGFloat)perPixelTolerance
pixelCount:(NSUInteger)pixelCount
referencePixels:(FBComparePixel *)referencePixel
imagePixels:(FBComparePixel *)imagePixel
differentPixels:(NSMutableArray **)differentPixels
{
NSUInteger numDiffPixels = 0;
for (NSUInteger n = 0; n < pixelCount; ++n) {
// If this pixel is different, increment the pixel diff count and see
// if we have hit our limit.
BOOL isIdenticalPixel = [self _comparePixelWithPerPixelTolerance:perPixelTolerance referencePixel:referencePixel imagePixel:imagePixel];
if (!isIdenticalPixel) {
[*differentPixels addObject:[NSNumber numberWithInteger:n]];

numDiffPixels++;

CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount;
if (percent > overallTolerance) {
return NO;

if (!*differentPixels) {
CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount;
if (percent > overallTolerance) {
return NO;
}
}
}

referencePixel++;
imagePixel++;
}

if (*differentPixels) {
CGFloat percent = (CGFloat)numDiffPixels / (CGFloat)pixelCount;
if (percent > overallTolerance) {
return NO;
}
}

return YES;
}

Expand Down
2 changes: 1 addition & 1 deletion FBSnapshotTestCase/Categories/UIImage+Diff.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN

@interface UIImage (Diff)

- (UIImage *)fb_diffWithImage:(UIImage *)image;
- (UIImage *)fb_diffWithImage:(UIImage *)image differentPixels:(NSArray *)differentPixels;

@end

Expand Down
16 changes: 15 additions & 1 deletion FBSnapshotTestCase/Categories/UIImage+Diff.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

@implementation UIImage (Diff)

- (UIImage *)fb_diffWithImage:(UIImage *)image
- (UIImage *)fb_diffWithImage:(UIImage *)image differentPixels:(NSArray *)differentPixels
{
if (!image) {
return nil;
Expand All @@ -48,6 +48,20 @@ - (UIImage *)fb_diffWithImage:(UIImage *)image
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height));
CGContextEndTransparencyLayer(context);

if (differentPixels) {
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);

CGFloat screenScale = [[UIScreen mainScreen] scale];
const NSUInteger pixelCount = differentPixels.count;
for (NSUInteger n = 0; n < pixelCount; ++n) {
int pixelNumber = [differentPixels[n] intValue] / screenScale;
CGFloat x = (pixelNumber % ((int) roundf(self.size.width)));
CGFloat y = ((pixelNumber - x) / (self.size.width * screenScale));

CGContextFillRect(context, CGRectMake(x, y, 1, 1));
}
}
UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return returnImage;
Expand Down
5 changes: 5 additions & 0 deletions FBSnapshotTestCase/FBSnapshotTestCase.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (readwrite, nonatomic, assign) BOOL recordMode;

/**
When YES, will cause hightlighting different fixels on test failure.
*/
@property (readwrite, nonatomic, assign) BOOL highlightDifferentPixels;

/**
When set, allows fine-grained control over what you want the file names to include.

Expand Down
11 changes: 11 additions & 0 deletions FBSnapshotTestCase/FBSnapshotTestCase.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ - (void)setRecordMode:(BOOL)recordMode
_snapshotController.recordMode = recordMode;
}

- (BOOL)highlightDifferentPixels
{
return _snapshotController.highlightDifferentPixels;
}

- (void)setHighlightDifferentPixels:(BOOL)highlightDifferentPixels
{
NSAssert1(_snapshotController, @"%s cannot be called before [super setUp]", __FUNCTION__);
_snapshotController.highlightDifferentPixels = highlightDifferentPixels;
}

- (FBSnapshotTestCaseFileNameIncludeOption)fileNameOptions
{
return _snapshotController.fileNameOptions;
Expand Down
17 changes: 14 additions & 3 deletions FBSnapshotTestCase/FBSnapshotTestController.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ extern NSString *const FBDiffedImageKey;
*/
@property (readwrite, nonatomic, assign) BOOL recordMode;

/**
Highlight Different Pixels.
*/
@property (readwrite, nonatomic, assign) BOOL highlightDifferentPixels;

/**
When set, allows fine-grained control over what you want the file names to include.

Expand Down Expand Up @@ -173,12 +178,15 @@ extern NSString *const FBDiffedImageKey;
@param image The image to test against the reference.
@param overallTolerance The percentage of pixels that can differ and still be considered 'identical'.
@param errorPtr An error that indicates why the comparison failed if it does.
@param differentPixels An array of different pixel numbers.
@returns YES if the comparison succeeded and the images are the same(ish).
*/
- (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
overallTolerance:(CGFloat)overallTolerance
error:(NSError **)errorPtr;
error:(NSError **)errorPtr
differentPixels:(NSMutableArray **)differentPixels
;

/**
Performs a pixel-by-pixel comparison of the two images with an allowable margin of error.
Expand All @@ -187,13 +195,15 @@ extern NSString *const FBDiffedImageKey;
@param perPixelTolerance The percentage a given pixel's R,G,B and A components can differ and still be considered 'identical'.
@param overallTolerance The percentage of pixels that can differ and still be considered 'identical'.
@param errorPtr An error that indicates why the comparison failed if it does.
@param differentPixels An array of different pixel numbers.
@returns YES if the comparison succeeded and the images are the same(ish).
*/
- (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
perPixelTolerance:(CGFloat)perPixelTolerance
overallTolerance:(CGFloat)overallTolerance
error:(NSError **)errorPtr;
error:(NSError **)errorPtr
differentPixels:(NSMutableArray **)differentPixels;

/**
Saves the reference image and the test image to `failedOutputDirectory`.
Expand All @@ -208,7 +218,8 @@ extern NSString *const FBDiffedImageKey;
testImage:(UIImage *)testImage
selector:(SEL)selector
identifier:(nullable NSString *)identifier
error:(NSError **)errorPtr;
error:(NSError **)errorPtr
differentPixels:(NSArray *)differentPixels;
@end

NS_ASSUME_NONNULL_END
32 changes: 26 additions & 6 deletions FBSnapshotTestCase/FBSnapshotTestController.m
Original file line number Diff line number Diff line change
Expand Up @@ -139,25 +139,28 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
overallTolerance:(CGFloat)overallTolerance
error:(NSError **)errorPtr
differentPixels:(NSMutableArray **)differentPixels
{
return [self compareReferenceImage:referenceImage
toImage:image
perPixelTolerance:0
overallTolerance:overallTolerance
error:errorPtr];
error:errorPtr
differentPixels:differentPixels];
}

- (BOOL)compareReferenceImage:(UIImage *)referenceImage
toImage:(UIImage *)image
perPixelTolerance:(CGFloat)perPixelTolerance
overallTolerance:(CGFloat)overallTolerance
error:(NSError **)errorPtr
differentPixels:(NSMutableArray **)differentPixels
{
CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(referenceImage.CGImage), CGImageGetHeight(referenceImage.CGImage));
CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));

BOOL sameImageDimensions = CGSizeEqualToSize(referenceImageSize, imageSize);
if (sameImageDimensions && [referenceImage fb_compareWithImage:image perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance]) {
if (sameImageDimensions && [referenceImage fb_compareWithImage:image perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance differentPixels:differentPixels]) {
return YES;
}

Expand All @@ -173,7 +176,7 @@ - (BOOL)compareReferenceImage:(UIImage *)referenceImage
NSLocalizedFailureReasonErrorKey : errorReason,
FBReferenceImageKey : referenceImage,
FBCapturedImageKey : image,
FBDiffedImageKey : [referenceImage fb_diffWithImage:image],
FBDiffedImageKey : [referenceImage fb_diffWithImage:image differentPixels:*differentPixels],
}];
}
return NO;
Expand All @@ -184,8 +187,9 @@ - (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage
selector:(SEL)selector
identifier:(NSString *)identifier
error:(NSError **)errorPtr
differentPixels:(NSArray *)differentPixels
{
UIImage *diffImage = [referenceImage fb_diffWithImage:testImage];
UIImage *diffImage = [referenceImage fb_diffWithImage:testImage differentPixels:differentPixels];

[XCTContext runActivityNamed:identifier ?: NSStringFromSelector(selector) block:^(id<XCTActivity> _Nonnull activity) {
XCTAttachment *referenceAttachment = [XCTAttachment attachmentWithImage:referenceImage];
Expand Down Expand Up @@ -319,10 +323,26 @@ - (BOOL)_performPixelComparisonWithViewOrLayer:(id)viewOrLayer
UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr];
if (referenceImage != nil) {
UIImage *snapshot = [self _imageForViewOrLayer:viewOrLayer];
BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot perPixelTolerance:perPixelTolerance overallTolerance:overallTolerance error:errorPtr];

NSMutableArray *differentPixels = nil;
if (self.highlightDifferentPixels) {
differentPixels = [NSMutableArray array];
}

BOOL imagesSame = [self compareReferenceImage:referenceImage
toImage:snapshot
perPixelTolerance:perPixelTolerance
overallTolerance:overallTolerance
error:errorPtr
differentPixels:&differentPixels];
if (!imagesSame) {
NSError *saveError = nil;
if ([self saveFailedReferenceImage:referenceImage testImage:snapshot selector:selector identifier:identifier error:&saveError] == NO) {
if ([self saveFailedReferenceImage:referenceImage
testImage:snapshot
selector:selector
identifier:identifier
error:&saveError
differentPixels:differentPixels] == NO) {
NSLog(@"Error saving test images: %@", saveError);
}
}
Expand Down
21 changes: 14 additions & 7 deletions FBSnapshotTestCaseTests/FBSnapshotControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ - (void)testCompareReferenceImageToImageShouldBeEqual
id testClass = nil;
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
NSError *error = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]);
XCTAssertNil(error);
}

Expand All @@ -43,7 +44,8 @@ - (void)testCompareReferenceImageToImageShouldNotBeEqual
id testClass = nil;
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
NSError *error = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]);
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent);
}
Expand All @@ -59,7 +61,8 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldNotMatch
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
// With virtually no margin for error, this should fail to be equal
NSError *error = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.0001 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.0001 error:&error differentPixels:&differentPixels]);
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent);
}
Expand All @@ -75,7 +78,8 @@ - (void)testCompareReferenceImageWithVeryLowToleranceShouldMatch
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
// With some tolerance these should be considered the same
NSError *error = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.001 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:.001 error:&error differentPixels:&differentPixels]);
XCTAssertNil(error);
}

Expand All @@ -90,7 +94,8 @@ - (void)testCompareReferenceImageWithDifferentSizes
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
// With some tolerance these should be considered the same
NSError *error = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error differentPixels:&differentPixels]);
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferentSizes);
}
Expand Down Expand Up @@ -170,7 +175,8 @@ - (void)testCompareReferenceImageWithLowPixelToleranceShouldNotMatch
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
// With virtually no margin for error, this should fail to be equal
NSError *error = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertFalse([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error differentPixels:&differentPixels]);
XCTAssertNotNil(error);
XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent);
}
Expand All @@ -186,7 +192,8 @@ - (void)testCompareReferenceImageWithLowPixelToleranceShouldMatch
FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass];
// With some tolerance these should be considered the same
NSError *error = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error]);
NSMutableArray *differentPixels = nil;
XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage perPixelTolerance:.06 overallTolerance:0 error:&error differentPixels:&differentPixels]);
XCTAssertNil(error);
}

Expand Down