From 3fa343cb287dfefa65aa81edbe74a2596f990a90 Mon Sep 17 00:00:00 2001 From: Lukas Kukacka Date: Sat, 13 Apr 2019 12:31:21 +0200 Subject: [PATCH] Added support for optimised reference image PNGs This allows to use apps like ImageOptim.app to optimise recorded reference images. Optimising reference image can heavily reduce its size, meaning we need to store less binary data in repo. --- FBSnapshotTestCase.xcodeproj/project.pbxproj | 12 +++++++ .../Categories/UIImage+Compare.m | 31 +++++++++++------- .../FBSnapshotControllerTests.m | 14 ++++++++ .../square_with_graphics.png | Bin 0 -> 8976 bytes .../square_with_graphics_imageoptim.png | Bin 0 -> 6386 bytes README.md | 9 +++++ 6 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 FBSnapshotTestCaseTests/square_with_graphics.png create mode 100644 FBSnapshotTestCaseTests/square_with_graphics_imageoptim.png diff --git a/FBSnapshotTestCase.xcodeproj/project.pbxproj b/FBSnapshotTestCase.xcodeproj/project.pbxproj index f4eaf84..2be20ec 100644 --- a/FBSnapshotTestCase.xcodeproj/project.pbxproj +++ b/FBSnapshotTestCase.xcodeproj/project.pbxproj @@ -36,6 +36,10 @@ 827137A21C63AC0D00354E42 /* square-copy.png in Resources */ = {isa = PBXBuildFile; fileRef = B32447DA1AB78B5E00B1D6FF /* square-copy.png */; }; 827137A31C63AC0D00354E42 /* square.png in Resources */ = {isa = PBXBuildFile; fileRef = B32447DB1AB78B5E00B1D6FF /* square.png */; }; 827137A41C63AC0F00354E42 /* FBSnapshotControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B31988301AB784CB00B0A900 /* FBSnapshotControllerTests.m */; }; + A9E093ED2261EEA400B1EDE3 /* square_with_graphics_imageoptim.png in Resources */ = {isa = PBXBuildFile; fileRef = A9E093EB2261EEA300B1EDE3 /* square_with_graphics_imageoptim.png */; }; + A9E093EE2261EEA400B1EDE3 /* square_with_graphics_imageoptim.png in Resources */ = {isa = PBXBuildFile; fileRef = A9E093EB2261EEA300B1EDE3 /* square_with_graphics_imageoptim.png */; }; + A9E093EF2261EEA400B1EDE3 /* square_with_graphics.png in Resources */ = {isa = PBXBuildFile; fileRef = A9E093EC2261EEA400B1EDE3 /* square_with_graphics.png */; }; + A9E093F02261EEA400B1EDE3 /* square_with_graphics.png in Resources */ = {isa = PBXBuildFile; fileRef = A9E093EC2261EEA400B1EDE3 /* square_with_graphics.png */; }; B31987FC1AB782D100B0A900 /* FBSnapshotTestCase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B31987F01AB782D000B0A900 /* FBSnapshotTestCase.framework */; }; B31988281AB7849400B0A900 /* FBSnapshotTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = B31988201AB7849400B0A900 /* FBSnapshotTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; B31988291AB7849400B0A900 /* FBSnapshotTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = B31988211AB7849400B0A900 /* FBSnapshotTestCase.m */; }; @@ -80,6 +84,8 @@ 42F2B74320C0D7A400ABED24 /* rect_shade.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = rect_shade.png; sourceTree = ""; }; 8271377A1C63AB6F00354E42 /* FBSnapshotTestCase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FBSnapshotTestCase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 827137831C63AB7000354E42 /* FBSnapshotTestCase tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FBSnapshotTestCase tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + A9E093EB2261EEA300B1EDE3 /* square_with_graphics_imageoptim.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square_with_graphics_imageoptim.png; sourceTree = ""; }; + A9E093EC2261EEA400B1EDE3 /* square_with_graphics.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square_with_graphics.png; sourceTree = ""; }; B31987F01AB782D000B0A900 /* FBSnapshotTestCase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FBSnapshotTestCase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B31987F41AB782D000B0A900 /* FBSnapshotTestCase-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "FBSnapshotTestCase-Info.plist"; sourceTree = ""; }; B31987FB1AB782D100B0A900 /* FBSnapshotTestCase iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FBSnapshotTestCase iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -196,6 +202,8 @@ children = ( 42F2B74320C0D7A400ABED24 /* rect_shade.png */, B76C68271C6BD68100586E5B /* rect.png */, + A9E093EB2261EEA300B1EDE3 /* square_with_graphics_imageoptim.png */, + A9E093EC2261EEA400B1EDE3 /* square_with_graphics.png */, E5C2CD611B1F399A00669887 /* square_with_pixel.png */, B32447D91AB78B5E00B1D6FF /* square_with_text.png */, B32447DA1AB78B5E00B1D6FF /* square-copy.png */, @@ -380,11 +388,13 @@ buildActionMask = 2147483647; files = ( B76C682A1C6BD6D500586E5B /* rect.png in Resources */, + A9E093EE2261EEA400B1EDE3 /* square_with_graphics_imageoptim.png in Resources */, 827137A01C63AC0700354E42 /* square_with_pixel.png in Resources */, 827137A21C63AC0D00354E42 /* square-copy.png in Resources */, 827137A31C63AC0D00354E42 /* square.png in Resources */, 827137A11C63AC0900354E42 /* square_with_text.png in Resources */, 42F2B74520C0D7A400ABED24 /* rect_shade.png in Resources */, + A9E093F02261EEA400B1EDE3 /* square_with_graphics.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -400,11 +410,13 @@ buildActionMask = 2147483647; files = ( B76C68291C6BD6D200586E5B /* rect.png in Resources */, + A9E093ED2261EEA400B1EDE3 /* square_with_graphics_imageoptim.png in Resources */, B32447DC1AB78B5E00B1D6FF /* square_with_text.png in Resources */, E5C2CD621B1F399A00669887 /* square_with_pixel.png in Resources */, B32447DE1AB78B5E00B1D6FF /* square.png in Resources */, B32447DD1AB78B5E00B1D6FF /* square-copy.png in Resources */, 42F2B74420C0D7A400ABED24 /* rect_shade.png in Resources */, + A9E093EF2261EEA400B1EDE3 /* square_with_graphics.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FBSnapshotTestCase/Categories/UIImage+Compare.m b/FBSnapshotTestCase/Categories/UIImage+Compare.m index a52c965..f220462 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Compare.m +++ b/FBSnapshotTestCase/Categories/UIImage+Compare.m @@ -50,9 +50,15 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage)); NSAssert(CGSizeEqualToSize(referenceImageSize, imageSize), @"Images must be same size."); - // The images have the equal size, so we could use the smallest amount of bytes because of byte padding - size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage)); - size_t referenceImageSizeBytes = referenceImageSize.height * minBytesPerRow; + // Find image which requires more bytes in memory. We have to normalise both images to context requiring "bigger" memory representation. + // This allows comparing 2 visually identical images even if their bit representation in file can be different + // (for example reference images optimised using ImageOptim app). + // Because both images have the same pixel size, image with more bytes per row is the image dictating the context config for comparison. + UIImage *contextConfigImage = (CGImageGetBytesPerRow(image.CGImage) > CGImageGetBytesPerRow(self.CGImage) ? image : self); + + // Create contexts for both images using the same configuration. + size_t bytesPerRow = CGImageGetBytesPerRow(contextConfigImage.CGImage); + size_t referenceImageSizeBytes = referenceImageSize.height * bytesPerRow; void *referenceImagePixels = calloc(1, referenceImageSizeBytes); void *imagePixels = calloc(1, referenceImageSizeBytes); @@ -62,20 +68,23 @@ - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixel return NO; } + size_t bitsPerComponent = CGImageGetBitsPerComponent(contextConfigImage.CGImage); + CGBitmapInfo bitmapInfo = (CGBitmapInfo)kCGImageAlphaPremultipliedLast; + CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels, referenceImageSize.width, referenceImageSize.height, - CGImageGetBitsPerComponent(self.CGImage), - minBytesPerRow, - CGImageGetColorSpace(self.CGImage), - (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + bitsPerComponent, + bytesPerRow, + CGImageGetColorSpace(contextConfigImage.CGImage), + bitmapInfo); CGContextRef imageContext = CGBitmapContextCreate(imagePixels, imageSize.width, imageSize.height, - CGImageGetBitsPerComponent(image.CGImage), - minBytesPerRow, - CGImageGetColorSpace(image.CGImage), - (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + bitsPerComponent, + bytesPerRow, + CGImageGetColorSpace(contextConfigImage.CGImage), + bitmapInfo); if (!referenceImageContext || !imageContext) { CGContextRelease(referenceImageContext); diff --git a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m index 600165f..6d191b8 100644 --- a/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m +++ b/FBSnapshotTestCaseTests/FBSnapshotControllerTests.m @@ -48,6 +48,20 @@ - (void)testCompareReferenceImageToImageShouldNotBeEqual XCTAssertEqual(error.code, FBSnapshotTestControllerErrorCodeImagesDifferent); } +- (void)testCompareOptimisedReferenceImageToImageShouldBeEqual +{ + UIImage *referenceImage = [self _bundledImageNamed:@"square_with_graphics_imageoptim" type:@"png"]; + XCTAssertNotNil(referenceImage); + UIImage *testImage = [self _bundledImageNamed:@"square_with_graphics" type:@"png"]; + XCTAssertNotNil(testImage); + + id testClass = nil; + FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] initWithTestClass:testClass]; + NSError *error = nil; + XCTAssertTrue([controller compareReferenceImage:referenceImage toImage:testImage overallTolerance:0 error:&error]); + XCTAssertNil(error); +} + - (void)testCompareReferenceImageWithVeryLowToleranceShouldNotMatch { UIImage *referenceImage = [self _bundledImageNamed:@"square" type:@"png"]; diff --git a/FBSnapshotTestCaseTests/square_with_graphics.png b/FBSnapshotTestCaseTests/square_with_graphics.png new file mode 100644 index 0000000000000000000000000000000000000000..3be10867f4c407a95c0d45178dda58437a7b0816 GIT binary patch literal 8976 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