| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "GoldenImage.h" |
| |
| #import <XCTest/XCTest.h> |
| #import <os/log.h> |
| #include <sys/sysctl.h> |
| |
| @interface GoldenImage () |
| |
| @end |
| |
| @implementation GoldenImage |
| |
| - (instancetype)initWithGoldenNamePrefix:(NSString*)prefix { |
| self = [super init]; |
| if (self) { |
| _goldenName = [prefix stringByAppendingString:_platformName()]; |
| NSBundle* bundle = [NSBundle bundleForClass:[self class]]; |
| NSURL* goldenURL = [bundle URLForResource:_goldenName withExtension:@"png"]; |
| NSData* data = [NSData dataWithContentsOfURL:goldenURL]; |
| _image = [[UIImage alloc] initWithData:data]; |
| } |
| return self; |
| } |
| |
| - (BOOL)compareGoldenToImage:(UIImage*)image rmesThreshold:(double)rmesThreshold { |
| if (!self.image || !image) { |
| os_log_error(OS_LOG_DEFAULT, "GOLDEN DIFF FAILED: image does not exists."); |
| return NO; |
| } |
| CGImageRef imageRefA = [self.image CGImage]; |
| CGImageRef imageRefB = [image CGImage]; |
| |
| NSUInteger widthA = CGImageGetWidth(imageRefA); |
| NSUInteger heightA = CGImageGetHeight(imageRefA); |
| NSUInteger widthB = CGImageGetWidth(imageRefB); |
| NSUInteger heightB = CGImageGetHeight(imageRefB); |
| |
| if (widthA != widthB || heightA != heightB) { |
| os_log_error(OS_LOG_DEFAULT, "GOLDEN DIFF FAILED: images sizes do not match."); |
| return NO; |
| } |
| NSUInteger bytesPerPixel = 4; |
| NSUInteger size = widthA * heightA * bytesPerPixel; |
| NSMutableData* rawA = [NSMutableData dataWithLength:size]; |
| NSMutableData* rawB = [NSMutableData dataWithLength:size]; |
| |
| if (!rawA || !rawB) { |
| os_log_error(OS_LOG_DEFAULT, "GOLDEN DIFF FAILED: image data length do not match."); |
| return NO; |
| } |
| |
| CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
| |
| NSUInteger bytesPerRow = bytesPerPixel * widthA; |
| NSUInteger bitsPerComponent = 8; |
| CGContextRef contextA = |
| CGBitmapContextCreate(rawA.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, |
| colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); |
| |
| CGContextDrawImage(contextA, CGRectMake(0, 0, widthA, heightA), imageRefA); |
| CGContextRelease(contextA); |
| |
| CGContextRef contextB = |
| CGBitmapContextCreate(rawB.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, |
| colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); |
| CGColorSpaceRelease(colorSpace); |
| |
| CGContextDrawImage(contextB, CGRectMake(0, 0, widthA, heightA), imageRefB); |
| CGContextRelease(contextB); |
| |
| const char* apos = rawA.mutableBytes; |
| const char* bpos = rawB.mutableBytes; |
| double sum = 0.0; |
| for (size_t i = 0; i < size; ++i, ++apos, ++bpos) { |
| // Skip transparent pixels. |
| if (*apos == 0 && *bpos == 0 && i % 4 == 0) { |
| i += 3; |
| apos += 3; |
| bpos += 3; |
| } else { |
| double aval = *apos; |
| double bval = *bpos; |
| double diff = aval - bval; |
| sum += diff * diff; |
| } |
| } |
| double rmse = sqrt(sum / size); |
| if (rmse > rmesThreshold) { |
| os_log_error( |
| OS_LOG_DEFAULT, |
| "GOLDEN DIFF FAILED: image diff greater than threshold. Current diff: %@, threshold: %@", |
| @(rmse), @(rmesThreshold)); |
| return NO; |
| } |
| return YES; |
| } |
| |
| NS_INLINE NSString* _platformName() { |
| NSString* systemVersion = UIDevice.currentDevice.systemVersion; |
| NSString* simulatorName = |
| [[NSProcessInfo processInfo].environment objectForKey:@"SIMULATOR_DEVICE_NAME"]; |
| if (simulatorName) { |
| return [NSString stringWithFormat:@"%@_%@_simulator", simulatorName, systemVersion]; |
| } |
| |
| size_t size; |
| sysctlbyname("hw.model", NULL, &size, NULL, 0); |
| char* answer = malloc(size); |
| sysctlbyname("hw.model", answer, &size, NULL, 0); |
| |
| NSString* results = [NSString stringWithUTF8String:answer]; |
| free(answer); |
| return results; |
| } |
| |
| @end |