| /* |
| * Copyright 2012 ZXing authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #import "AbstractBlackBoxTestCase.h" |
| #import "TestResult.h" |
| |
| @implementation AbstractBlackBoxTestCase |
| |
| - (id)initWithInvocation:(NSInvocation *)invocation testBasePathSuffix:(NSString *)testBasePathSuffix barcodeReader:(id<ZXReader>)barcodeReader expectedFormat:(ZXBarcodeFormat)expectedFormat { |
| if (self = [super initWithInvocation:invocation]) { |
| _testBase = testBasePathSuffix; |
| _barcodeReader = barcodeReader; |
| _expectedFormat = expectedFormat; |
| _testResults = [NSMutableArray array]; |
| } |
| |
| return self; |
| } |
| |
| - (void)addTest:(int)mustPassCount tryHarderCount:(int)tryHarderCount rotation:(float)rotation { |
| [self addTest:mustPassCount tryHarderCount:tryHarderCount maxMisreads:0 maxTryHarderMisreads:0 rotation:rotation]; |
| } |
| |
| /** |
| * Adds a new test for the current directory of images. |
| */ |
| - (void)addTest:(int)mustPassCount tryHarderCount:(int)tryHarderCount maxMisreads:(int)maxMisreads maxTryHarderMisreads:(int)maxTryHarderMisreads rotation:(float)rotation { |
| [self.testResults addObject:[[TestResult alloc] initWithMustPassCount:mustPassCount tryHarderCount:tryHarderCount maxMisreads:maxMisreads maxTryHarderMisreads:maxTryHarderMisreads rotation:rotation]]; |
| } |
| |
| - (NSArray *)imageFiles { |
| NSMutableArray *imageFiles = [NSMutableArray array]; |
| for (NSString *file in [[NSBundle bundleForClass:[self class]] pathsForResourcesOfType:nil inDirectory:self.testBase]) { |
| if ([[[file pathExtension] lowercaseString] isEqualToString:@"jpg"] || |
| [[[file pathExtension] lowercaseString] isEqualToString:@"jpeg"] || |
| [[[file pathExtension] lowercaseString] isEqualToString:@"gif"] || |
| [[[file pathExtension] lowercaseString] isEqualToString:@"png"]) { |
| [imageFiles addObject:[NSURL fileURLWithPath:file]]; |
| } |
| } |
| |
| return imageFiles; |
| } |
| |
| - (void)runTests { |
| [self testBlackBoxCountingResults:YES]; |
| } |
| |
| + (NSString *)barcodeFormatAsString:(ZXBarcodeFormat)format { |
| switch (format) { |
| case kBarcodeFormatAztec: |
| return @"Aztec"; |
| break; |
| case kBarcodeFormatCodabar: |
| return @"CODABAR"; |
| break; |
| case kBarcodeFormatCode39: |
| return @"Code 39"; |
| break; |
| case kBarcodeFormatCode93: |
| return @"Code 93"; |
| break; |
| case kBarcodeFormatCode128: |
| return @"Code 128"; |
| break; |
| case kBarcodeFormatDataMatrix: |
| return @"Data Matrix"; |
| break; |
| case kBarcodeFormatEan8: |
| return @"EAN-8"; |
| break; |
| case kBarcodeFormatEan13: |
| return @"EAN-13"; |
| break; |
| case kBarcodeFormatITF: |
| return @"ITF"; |
| break; |
| case kBarcodeFormatMaxiCode: |
| return @"MaxiCode"; |
| break; |
| case kBarcodeFormatPDF417: |
| return @"PDF417"; |
| break; |
| case kBarcodeFormatQRCode: |
| return @"QR Code"; |
| break; |
| case kBarcodeFormatRSS14: |
| return @"RSS 14"; |
| break; |
| case kBarcodeFormatRSSExpanded: |
| return @"RSS EXPANDED"; |
| break; |
| case kBarcodeFormatUPCA: |
| return @"UPC-A"; |
| break; |
| case kBarcodeFormatUPCE: |
| return @"UPC-E"; |
| break; |
| case kBarcodeFormatUPCEANExtension: |
| return @"UPC/EAN extension"; |
| break; |
| } |
| |
| return nil; |
| } |
| |
| - (NSString *)pathInBundle:(NSURL *)file { |
| NSInteger startOfResources = [[file path] rangeOfString:@"Resources"].location; |
| if (startOfResources == NSNotFound) { |
| return [file path]; |
| } else { |
| return [[file path] substringFromIndex:startOfResources]; |
| } |
| } |
| |
| - (void)testBlackBoxCountingResults:(BOOL)assertOnFailure { |
| if (self.testResults.count == 0) { |
| STFail(@"No test results"); |
| } |
| |
| NSFileManager *fileManager = [NSFileManager defaultManager]; |
| NSArray *imageFiles = [self imageFiles]; |
| int testCount = (int)[self.testResults count]; |
| |
| int passedCounts[testCount]; |
| memset(passedCounts, 0, testCount * sizeof(int)); |
| |
| int misreadCounts[testCount]; |
| memset(misreadCounts, 0, testCount * sizeof(int)); |
| |
| int tryHarderCounts[testCount]; |
| memset(tryHarderCounts, 0, testCount * sizeof(int)); |
| |
| int tryHarderMisreadCounts[testCount]; |
| memset(tryHarderMisreadCounts, 0, testCount * sizeof(int)); |
| |
| for (NSURL *testImage in imageFiles) { |
| NSLog(@"Starting %@", [self pathInBundle:testImage]); |
| |
| ZXImage *image = [[ZXImage alloc] initWithURL:testImage]; |
| |
| NSString *testImageFileName = [[[testImage path] componentsSeparatedByString:@"/"] lastObject]; |
| NSString *fileBaseName = [testImageFileName substringToIndex:[testImageFileName rangeOfString:@"."].location]; |
| NSString *expectedTextFile = [[NSBundle bundleForClass:[self class]] pathForResource:fileBaseName ofType:@"txt" inDirectory:self.testBase]; |
| |
| NSString *expectedText; |
| if (expectedTextFile) { |
| expectedText = [self readFileAsString:expectedTextFile encoding:NSUTF8StringEncoding]; |
| } else { |
| NSString *expectedTextFile = [[NSBundle bundleForClass:[self class]] pathForResource:fileBaseName ofType:@"bin" inDirectory:self.testBase]; |
| STAssertNotNil(expectedTextFile, @"Expected text does not exist"); |
| expectedText = [self readFileAsString:expectedTextFile encoding:NSISOLatin1StringEncoding]; |
| } |
| |
| NSURL *expectedMetadataFile = [NSURL URLWithString:[[NSBundle bundleForClass:[self class]] pathForResource:fileBaseName ofType:@".metadata.txt" inDirectory:self.testBase]]; |
| NSMutableDictionary *expectedMetadata = [NSMutableDictionary dictionary]; |
| if ([fileManager fileExistsAtPath:[expectedMetadataFile path]]) { |
| expectedMetadata = [NSMutableDictionary dictionaryWithContentsOfFile:[expectedMetadataFile path]]; |
| } |
| |
| for (int x = 0; x < testCount; x++) { |
| float rotation = [(TestResult *)self.testResults[x] rotation]; |
| ZXImage *rotatedImage = [self rotateImage:image degrees:rotation]; |
| ZXLuminanceSource *source = [[ZXCGImageLuminanceSource alloc] initWithCGImage:rotatedImage.cgimage]; |
| ZXBinaryBitmap *bitmap = [[ZXBinaryBitmap alloc] initWithBinarizer:[[ZXHybridBinarizer alloc] initWithSource:source]]; |
| BOOL misread; |
| if ([self decode:bitmap rotation:rotation expectedText:expectedText expectedMetadata:expectedMetadata tryHarder:NO misread:&misread]) { |
| passedCounts[x]++; |
| } else if(misread) { |
| misreadCounts[x]++; |
| } else { |
| NSLog(@"could not read at rotation %f", rotation); |
| } |
| |
| if ([self decode:bitmap rotation:rotation expectedText:expectedText expectedMetadata:expectedMetadata tryHarder:YES misread:&misread]) { |
| tryHarderCounts[x]++; |
| } else if(misread) { |
| tryHarderMisreadCounts[x]++; |
| } else { |
| NSLog(@"could not read at rotation %f w/TH", rotation); |
| } |
| } |
| } |
| |
| // Print the results of all tests first |
| int totalFound = 0; |
| int totalMustPass = 0; |
| int totalMisread = 0; |
| int totalMaxMisread = 0; |
| |
| for (int x = 0; x < testCount; x++) { |
| TestResult *testResult = self.testResults[x]; |
| NSLog(@"Rotation %d degrees:", (int) testResult.rotation); |
| NSLog(@" %d of %d images passed (%d required)", |
| passedCounts[x], (int)imageFiles.count, testResult.mustPassCount); |
| int failed = (int)imageFiles.count - passedCounts[x]; |
| NSLog(@" %d failed due to misreads, %d not detected", |
| misreadCounts[x], failed - misreadCounts[x]); |
| NSLog(@" %d of %d images passed with try harder (%d required)", |
| tryHarderCounts[x], (int)imageFiles.count, testResult.tryHarderCount); |
| failed = (int)imageFiles.count - tryHarderCounts[x]; |
| NSLog(@" %d failed due to misreads, %d not detected", |
| tryHarderMisreadCounts[x], failed - tryHarderMisreadCounts[x]); |
| totalFound += passedCounts[x] + tryHarderCounts[x]; |
| totalMustPass += testResult.mustPassCount + testResult.tryHarderCount; |
| totalMisread += misreadCounts[x] + tryHarderMisreadCounts[x]; |
| totalMaxMisread += testResult.maxMisreads + testResult.maxTryHarderMisreads; |
| } |
| |
| int totalTests = (int)imageFiles.count * testCount * 2; |
| NSLog(@"TOTALS:\nDecoded %d images out of %d (%d%%, %d required)", |
| totalFound, totalTests, totalFound * 100 / totalTests, totalMustPass); |
| if (totalFound > totalMustPass) { |
| NSLog(@" +++ Test too lax by %d images", totalFound - totalMustPass); |
| } else if (totalFound < totalMustPass) { |
| NSLog(@" --- Test failed by %d images", totalMustPass - totalFound); |
| } |
| |
| if (totalMisread < totalMaxMisread) { |
| NSLog(@" +++ Test expects too many misreads by %d images", totalMaxMisread - totalMisread); |
| } else if (totalMisread > totalMaxMisread) { |
| NSLog(@" --- Test had too many misreads by %d images", totalMisread - totalMaxMisread); |
| } |
| |
| // Then run through again and assert if any failed |
| if (assertOnFailure) { |
| for (int x = 0; x < testCount; x++) { |
| TestResult *testResult = self.testResults[x]; |
| NSString *label = [NSString stringWithFormat:@"Rotation %f degrees: Too many images failed", testResult.rotation]; |
| STAssertTrue(passedCounts[x] >= testResult.mustPassCount, label); |
| STAssertTrue(tryHarderCounts[x] >= testResult.tryHarderCount, @"Try harder, %@", label); |
| label = [NSString stringWithFormat:@"Rotation %f degrees: Too many images misread", testResult.rotation]; |
| STAssertTrue(misreadCounts[x] <= testResult.maxMisreads, label); |
| STAssertTrue(tryHarderMisreadCounts[x] <= testResult.maxTryHarderMisreads, @"Try harder, %@", label); |
| } |
| } |
| } |
| |
| - (BOOL)decode:(ZXBinaryBitmap *)source rotation:(float)rotation expectedText:(NSString *)expectedText expectedMetadata:(NSMutableDictionary *)expectedMetadata tryHarder:(BOOL)tryHarder misread:(BOOL *)misread { |
| NSString *suffix = [NSString stringWithFormat:@" (%@rotation: %d)", tryHarder ? @"try harder, " : @"", (int) rotation]; |
| *misread = NO; |
| |
| ZXDecodeHints *hints = [ZXDecodeHints hints]; |
| if (tryHarder) { |
| hints.tryHarder = YES; |
| } |
| |
| ZXResult *result = [self.barcodeReader decode:source hints:hints error:nil]; |
| if (!result) { |
| return NO; |
| } |
| |
| if (self.expectedFormat != result.barcodeFormat) { |
| NSLog(@"Format mismatch: expected '%@' but got '%@'%@", |
| [[self class] barcodeFormatAsString:self.expectedFormat], [[self class] barcodeFormatAsString:result.barcodeFormat], suffix); |
| *misread = YES; |
| return NO; |
| } |
| |
| NSString *resultText = result.text; |
| if (![expectedText isEqualToString:resultText]) { |
| NSLog(@"Content mismatch: expected '%@' but got '%@'%@", expectedText, resultText, suffix); |
| *misread = YES; |
| return NO; |
| } |
| |
| NSMutableDictionary *resultMetadata = result.resultMetadata; |
| for (id keyObj in [expectedMetadata allKeys]) { |
| ZXResultMetadataType key = [keyObj intValue]; |
| id expectedValue = expectedMetadata[keyObj]; |
| id actualValue = resultMetadata[keyObj]; |
| if (![expectedValue isEqual:actualValue]) { |
| NSLog(@"Metadata mismatch: for key '%d' expected '%@' but got '%@'", key, expectedValue, actualValue); |
| *misread = YES; |
| return NO; |
| } |
| } |
| |
| return YES; |
| } |
| |
| - (NSString *)readFileAsString:(NSString *)file encoding:(NSStringEncoding)encoding { |
| NSString *stringContents = [NSString stringWithContentsOfFile:file encoding:encoding error:nil]; |
| if ([stringContents hasSuffix:@"\n"]) { |
| NSLog(@"String contents of file %@ end with a newline. This may not be intended and cause a test failure", file); |
| } |
| return stringContents; |
| } |
| |
| // Adapted from http://blog.coriolis.ch/2009/09/04/arbitrary-rotation-of-a-cgimage/ and https://github.com/JanX2/CreateRotateWriteCGImage |
| - (ZXImage *)rotateImage:(ZXImage *)original degrees:(float)degrees { |
| if (degrees == 0.0f) { |
| return original; |
| } |
| double radians = -1 * degrees * (M_PI / 180); |
| |
| CGRect imgRect = CGRectMake(0, 0, original.width, original.height); |
| CGAffineTransform transform = CGAffineTransformMakeRotation(radians); |
| CGRect rotatedRect = CGRectApplyAffineTransform(imgRect, transform); |
| |
| CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
| CGContextRef context = CGBitmapContextCreate(NULL, |
| rotatedRect.size.width, |
| rotatedRect.size.height, |
| CGImageGetBitsPerComponent(original.cgimage), |
| 0, |
| colorSpace, |
| kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedFirst); |
| CGContextSetAllowsAntialiasing(context, FALSE); |
| CGContextSetInterpolationQuality(context, kCGInterpolationNone); |
| CGColorSpaceRelease(colorSpace); |
| |
| CGContextTranslateCTM(context, |
| +(rotatedRect.size.width/2), |
| +(rotatedRect.size.height/2)); |
| CGContextRotateCTM(context, radians); |
| |
| CGContextDrawImage(context, CGRectMake(-imgRect.size.width/2, |
| -imgRect.size.height/2, |
| imgRect.size.width, |
| imgRect.size.height), |
| original.cgimage); |
| |
| CGImageRef rotatedImage = CGBitmapContextCreateImage(context); |
| |
| CFRelease(context); |
| |
| return [[ZXImage alloc] initWithCGImageRef:rotatedImage]; |
| } |
| |
| @end |