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 0000000..3be1086 Binary files /dev/null and b/FBSnapshotTestCaseTests/square_with_graphics.png differ diff --git a/FBSnapshotTestCaseTests/square_with_graphics_imageoptim.png b/FBSnapshotTestCaseTests/square_with_graphics_imageoptim.png new file mode 100644 index 0000000..60ff098 Binary files /dev/null and b/FBSnapshotTestCaseTests/square_with_graphics_imageoptim.png differ diff --git a/README.md b/README.md index 157b5c1..d0b7d56 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Define the `IMAGE_DIFF_DIR` to the directory where you want to store diffs of fa - Support for `CALayer` via `FBSnapshotVerifyLayer`. - `usesDrawViewHierarchyInRect` to handle cases like `UIVisualEffect`, `UIAppearance` and Size Classes. - `fileNameOptions` to control appending the device model (`iPhone`, `iPad`, `iPod Touch`, etc), OS version, screen size and screen scale to the images (allowing to have multiple tests for the same «snapshot» for different `OS`s and devices). +- Support of reference images optimized for reduced size. ## Notes @@ -95,6 +96,14 @@ should be run within the Simulator so that it has access to UIKit.) Read more on this [here](docs/LibraryVsApplicationTestBundles.md). +## Optimizing reference images to reduce size + +Recorded reference image PNG can be optimised to reduce its size using lossless compression. + +Once you have final reference images recorded, you can manually optimise them using tools like [ImageOptim](https://imageoptim.com). ImageOptim will reduce size of the image without loss of any visual information. The savings in image sizes can be significant (up to 80%+ depending on the original image). + +If you are committing reference images to repository, optimizing them reduces their impact growing to repo size. + ## Authors `iOSSnapshotTestCase` was written at Facebook by