diff --git a/OGImage/OGImageCache.m b/OGImage/OGImageCache.m index 781935d..337e72a 100644 --- a/OGImage/OGImageCache.m +++ b/OGImage/OGImageCache.m @@ -41,7 +41,7 @@ + (OGImageCache *)shared { + (NSString *)MD5:(NSString *)string { const char *d = [string UTF8String]; unsigned char r[CC_MD5_DIGEST_LENGTH]; - CC_MD5(d, strlen(d), r); + CC_MD5(d, (CC_LONG)strlen(d), r); NSMutableString *hexString = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH]; for (int ii = 0; ii < CC_MD5_DIGEST_LENGTH; ++ii) { [hexString appendFormat:@"%02x", r[ii]]; @@ -104,8 +104,13 @@ - (void)imageForKey:(NSString *)key block:(OGImageCacheCompletionBlock)block { } - (void)setImage:(__OGImage *)image forKey:(NSString *)key { + // assert for developers, and guard against production crashes NSParameterAssert(nil != image); NSParameterAssert(nil != key); + if (nil == image || nil == key) { + return; + } + [_memoryCache setObject:image forKey:key]; dispatch_async(_cacheFileTasksQueue, ^{ NSURL *fileURL = [OGImageCache fileURLForKey:key]; diff --git a/OGImage/OGImageProcessing.m b/OGImage/OGImageProcessing.m index 3d245d2..9bb454a 100644 --- a/OGImage/OGImageProcessing.m +++ b/OGImage/OGImageProcessing.m @@ -88,15 +88,21 @@ OSStatus UIImageToVImageBuffer(UIImage *image, vImage_Buffer *buffer, CGImageAlp buffer->width, buffer->height, 8, buffer->rowBytes, colorSpace, alphaInfo); - if (UIImageOrientationRight == image.imageOrientation) { - CGContextRotateCTM(ctx, -M_PI/2.f); - CGContextTranslateCTM(ctx, -(CGFloat)height, 0.f); - } else if (UIImageOrientationLeft == image.imageOrientation) { - CGContextRotateCTM(ctx, M_PI/2.f); - CGContextTranslateCTM(ctx, 0.f, -(CGFloat)width); + if (NULL == ctx) { + free(buffer->data); + buffer->data = NULL; + err = OGImageProcessingError; + } else { + if (UIImageOrientationRight == image.imageOrientation) { + CGContextRotateCTM(ctx, -M_PI/2.f); + CGContextTranslateCTM(ctx, -(CGFloat)height, 0.f); + } else if (UIImageOrientationLeft == image.imageOrientation) { + CGContextRotateCTM(ctx, M_PI/2.f); + CGContextTranslateCTM(ctx, 0.f, -(CGFloat)width); + } + CGContextDrawImage(ctx, CGRectMake(0.f, 0.f, CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)), cgImage); + CGContextRelease(ctx); } - CGContextDrawImage(ctx, CGRectMake(0.f, 0.f, CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)), cgImage); - CGContextRelease(ctx); CGColorSpaceRelease(colorSpace); return err; } @@ -184,6 +190,9 @@ - (void)scaleImage:(__OGImage *)image toSize:(CGSize)size cornerRadius:(CGFloat) if (kCGImageAlphaNone == alphaInfo) { // kCGImageAlphaNone w/8-bit channels not supported alphaInfo = kCGImageAlphaNoneSkipLast; + } else if (kCGImageAlphaFirst == alphaInfo || kCGImageAlphaLast == alphaInfo) { + // non-premultiplied contexts are not supported + alphaInfo = kCGImageAlphaPremultipliedFirst; } if (0.f < cornerRadius) { alphaInfo = kCGImageAlphaPremultipliedFirst; diff --git a/OGImage/OGImageRequest.m b/OGImage/OGImageRequest.m index a666a87..0c6f695 100644 --- a/OGImage/OGImageRequest.m +++ b/OGImage/OGImageRequest.m @@ -75,7 +75,7 @@ - (void)prepareImageAndNotify { } } else { // if we get here, we have an http status code other than 200 - tmpError = [NSError errorWithDomain:NSCocoaErrorDomain code:OGImageLoadingError userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"OGImage: Received http status code: %d", _httpResponse.statusCode]}]; + tmpError = [NSError errorWithDomain:NSCocoaErrorDomain code:OGImageLoadingError userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"OGImage: Received http status code: %ld", (long)_httpResponse.statusCode]}]; } NSAssert((nil == tmpImage && nil != tmpError) || (nil != tmpImage && nil == tmpError), @"One of tmpImage or tmpError should be non-nil"); dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/OGImage/OGImageView.h b/OGImage/OGImageView.h new file mode 100644 index 0000000..e3d8b86 --- /dev/null +++ b/OGImage/OGImageView.h @@ -0,0 +1,32 @@ +// +// OGImageView.h +// OGImageDemo +// +// Created by Art Gillespie on 8/23/13. +// Copyright (c) 2013 Origami Labs. All rights reserved. +// + +#import + +@interface OGImageView : UIImageView + +/** + * Set image view's image with the image at `url`. `OGImageView` supports the following + * protocols: + * + * * `http` + * * `file` + * * `assets-library` + * + * The image will be scaled and fit according to the view's `bounds` and `contentMode`, + * respectively. + */ +- (void)setImageURL:(NSURL *)url placeholder:(UIImage *)image; + +/** + * If there's a problem loading the image, this property will be set. KVO-observable. + * @see `OGImage.error` + */ +@property (nonatomic, strong, readonly) NSError *imageError; + +@end diff --git a/OGImage/OGImageView.m b/OGImage/OGImageView.m new file mode 100644 index 0000000..e0a170c --- /dev/null +++ b/OGImage/OGImageView.m @@ -0,0 +1,52 @@ +// +// OGImageView.m +// OGImageDemo +// +// Created by Art Gillespie on 8/23/13. +// Copyright (c) 2013 Origami Labs. All rights reserved. +// + +#import "OGImageView.h" +#import "OGScaledImage.h" + +static NSString *KVOContext = @"OGImageView observation"; + +@implementation OGImageView { + OGScaledImage *_scaledImage; +} + +- (void)setImageURL:(NSURL *)url placeholder:(UIImage *)placeholder { + self.image = placeholder; + [_scaledImage removeObserver:self context:&KVOContext]; + OGImageProcessingScaleMethod scaleMethod = OGImageProcessingScale_AspectFill; + if (UIViewContentModeScaleAspectFit == self.contentMode) { + scaleMethod = OGImageProcessingScale_AspectFit; + } + _scaledImage = [[OGScaledImage alloc] initWithURL:url size:self.bounds.size cornerRadius:0.f method:scaleMethod key:nil placeholderImage:nil]; + [_scaledImage addObserver:self context:&KVOContext]; + if (nil != _scaledImage.scaledImage) { + self.image = _scaledImage.scaledImage; + } +} + +#pragma mark KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (((void *)&KVOContext) == context) { + if ([@"scaledImage" isEqualToString:keyPath]) { + self.image = _scaledImage.scaledImage; + } else if ([@"error" isEqualToString:keyPath]) { + [self willChangeValueForKey:@"imageError"]; + _imageError = _scaledImage.error; + [self didChangeValueForKey:@"imageError"]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)dealloc { + [_scaledImage removeObserver:self context:&KVOContext]; +} + +@end diff --git a/OGImage/OGScaledImage.m b/OGImage/OGScaledImage.m index 9424332..7ab6e12 100644 --- a/OGImage/OGScaledImage.m +++ b/OGImage/OGScaledImage.m @@ -8,8 +8,8 @@ #import "OGScaledImage.h" #import "OGImageCache.h" -NSString *OGKeyWithSize(NSString *origKey, CGSize size, CGFloat cornerRadius) { - return [NSString stringWithFormat:@"%@-%f-%f-%f", origKey, size.width, size.height, cornerRadius]; +NSString *OGKeyWithSize(NSString *origKey, CGSize size, CGFloat cornerRadius, OGImageProcessingScaleMethod method) { + return [NSString stringWithFormat:@"%@-%f-%f-%f-%ld", origKey, size.width, size.height, cornerRadius, (long)method]; } @implementation OGScaledImage { @@ -45,7 +45,7 @@ - (id)initWithURL:(NSURL *)url size:(CGSize)size cornerRadius:(CGFloat)cornerRad self.scaledImage = placeholderImage; _scaledSize = size; _cornerRadius = cornerRadius; - _scaledKey = OGKeyWithSize(self.key, _scaledSize, _cornerRadius); + _scaledKey = OGKeyWithSize(self.key, _scaledSize, _cornerRadius, method); [self loadImageFromURL]; } return self; diff --git a/OGImage/__OGImage.h b/OGImage/__OGImage.h index 872dff9..6973c42 100644 --- a/OGImage/__OGImage.h +++ b/OGImage/__OGImage.h @@ -18,16 +18,17 @@ * * When you create an __OGImage with a fileURL or NSData instance, it uses the * Image I/O framework under the hood to find out about the file's format, metadata - * and alpha. Additionally, if the file ends with the extension `.@2x`, __OGImage - * will set UIImage's `scale` property correctly. + * and alpha. Additionally, if the file ends with an extension like `.@2x`, + * __OGImage will set UIImage's `scale` property correctly. * * When you save an __OGImage using `writeToURL`, it will automatically choose * the best format based on the current alpha properties and original file format. - * Additionally, if the superclass' property `scale` is `2.f`, `writeToURL` will - * automatically append the `.@2x` suffix. (Ultimately, this `.@2x` stuff is - * an implementation detail: If you're not worried about cache internals, you - * don't need to worry about this. Just use keys normally and `__OGImage` and - * friends will figure everything out for you. + * Additionally, if the superclass' property `scale` is > 1, `writeToURL` will + * automatically append a resolution suffix like `.@2x`. + * + * (Ultimately, this `.@2x` stuff is an implementation detail: If you're not + * worried about cache internals, you don't need to worry about this. Just use + * keys normally and `__OGImage` and friends will figure everything out for you. */ @interface __OGImage : UIImage diff --git a/OGImage/__OGImage.m b/OGImage/__OGImage.m index 54e460f..e265af5 100644 --- a/OGImage/__OGImage.m +++ b/OGImage/__OGImage.m @@ -35,17 +35,39 @@ UIImageOrientation OGEXIFOrientationToUIImageOrientation(NSInteger exif) { } } +NSString *OGResolutionSuffixForScale(CGFloat scale) { + return [NSString stringWithFormat:@"@%.0fx", scale]; +} + @implementation __OGImage - (id)initWithDataAtURL:(NSURL *)url { CGFloat scale = 1.f; if (NO == [[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { - url = [url URLByAppendingPathExtension:@"@2x"]; - if (NO == [[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { - // no such file + // handling of scaled images is still limited, but at a practical level, the odds of seeing scale factors > 10 seem pretty long + // try to optimize by starting with screen rez of device + // possible that directory enumeration might be faster? but generally, hard to prove the negative proposition that there's no file + scale = [[UIScreen mainScreen] scale]; + NSURL *scaledURL = [url URLByAppendingPathExtension:OGResolutionSuffixForScale(scale)]; + if (YES == [[NSFileManager defaultManager] fileExistsAtPath:[scaledURL path]]) { + url = scaledURL; + } + else { + // no file at the device resolution; try others + // ??? or should we refuse to load images at mis-matched rez? + for (scale = 2.f; scale < 11.f; ++scale) { + if( scale != [[UIScreen mainScreen] scale] ) { + scaledURL = [url URLByAppendingPathExtension:OGResolutionSuffixForScale(scale)]; + if (YES == [[NSFileManager defaultManager] fileExistsAtPath:[scaledURL path]]) { + url = scaledURL; + break; + } + } + } + } + if( url != scaledURL ) { return nil; } - scale = 2.f; } NSData *data = [NSData dataWithContentsOfURL:url]; return [self initWithData:data scale:scale]; @@ -89,8 +111,7 @@ - (id)initWithData:(NSData *)data scale:(CGFloat)scale { // do we have an OGImageDictionary? _originalFileType = (__bridge NSString *)CGImageSourceGetType(imageSource); _originalFileAlphaInfo = CGImageGetAlphaInfo(cgImage); - NSDictionary *propDict = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL)); - _originalFileOrientation = [propDict[(__bridge NSString *)kCGImagePropertyOrientation] integerValue]; + _originalFileOrientation = [_originalFileProperties[(__bridge NSString *)kCGImagePropertyOrientation] integerValue]; self = [super initWithCGImage:cgImage scale:scale orientation:OGEXIFOrientationToUIImageOrientation(_originalFileOrientation)]; CGImageRelease(cgImage); } @@ -115,8 +136,8 @@ - (BOOL)writeToURL:(NSURL *)fileURL error:(NSError **)error { imgType = @"public.jpeg"; } } - if (2.f == self.scale) { - fileURL = [fileURL URLByAppendingPathExtension:@"@2x"]; + if (1.f < self.scale) { + fileURL = [fileURL URLByAppendingPathExtension:OGResolutionSuffixForScale(self.scale)]; } CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL((__bridge CFURLRef)fileURL, (__bridge CFStringRef)imgType, 1, NULL); if (NULL == imageDestination) { diff --git a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj index 5856587..bb18248 100644 --- a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj +++ b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 416DC6759A8046D6AED27FFE /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 467479B3192A45B8984313B7 /* libPods.a */; }; + 9C46672118D39709008960BA /* moldex-logo.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9C46672018D39709008960BA /* moldex-logo.gif */; }; + 9C46672318D39EA7008960BA /* OGImageProblematicProcessingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C46672218D39EA7008960BA /* OGImageProblematicProcessingTests.m */; }; C803062A166526A900073395 /* james_bond.json in Resources */ = {isa = PBXBuildFile; fileRef = C8030629166526A900073395 /* james_bond.json */; }; C803062D1665295A00073395 /* OGImageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C803062C1665295A00073395 /* OGImageTableViewCell.m */; }; C803063016652E8200073395 /* placeholder.png in Resources */ = {isa = PBXBuildFile; fileRef = C803062E16652E8200073395 /* placeholder.png */; }; @@ -57,15 +59,20 @@ C88272321663DBDF007D409F /* OGViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C88272311663DBDF007D409F /* OGViewController.m */; }; C88272351663DBDF007D409F /* OGViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C88272331663DBDF007D409F /* OGViewController.xib */; }; C882723E1663E5B0007D409F /* OGImage.m in Sources */ = {isa = PBXBuildFile; fileRef = C882723D1663E5B0007D409F /* OGImage.m */; }; + C88AE28F17C7EDD4002A34C7 /* OGImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = C88AE28E17C7EDD4002A34C7 /* OGImageView.m */; }; C8975AEA166E09BB00C2D53A /* OGScaledImage.m in Sources */ = {isa = PBXBuildFile; fileRef = C8975AE9166E09BB00C2D53A /* OGScaledImage.m */; }; C8975AEB166E09BB00C2D53A /* OGScaledImage.m in Sources */ = {isa = PBXBuildFile; fileRef = C8975AE9166E09BB00C2D53A /* OGScaledImage.m */; }; C8FADED9166D5DB100829179 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8FADED8166D5DB100829179 /* Accelerate.framework */; }; C8FADEDB166D5DCA00829179 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8FADED8166D5DB100829179 /* Accelerate.framework */; }; + F68541041A096AED001D6727 /* OGImageScaleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F68541031A096AED001D6727 /* OGImageScaleTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 467479B3192A45B8984313B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = SOURCE_ROOT; }; + 613EC03F3D03DD7743B0B058 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 623F95686DC456957AC440BC /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 9C46672018D39709008960BA /* moldex-logo.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "moldex-logo.gif"; sourceTree = ""; }; + 9C46672218D39EA7008960BA /* OGImageProblematicProcessingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OGImageProblematicProcessingTests.m; sourceTree = ""; }; C8030629166526A900073395 /* james_bond.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = james_bond.json; path = "Demo Data/james_bond.json"; sourceTree = ""; }; C803062B1665295A00073395 /* OGImageTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OGImageTableViewCell.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; C803062C1665295A00073395 /* OGImageTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageTableViewCell.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; @@ -85,9 +92,9 @@ C83BE57C16C5904600D82A1A /* __OGImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = __OGImage.h; sourceTree = ""; }; C83BE57D16C5904600D82A1A /* __OGImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = __OGImage.m; sourceTree = ""; }; C84972AF166564D000DB15D1 /* OGImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OGImageCache.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - C84972B0166564D000DB15D1 /* OGImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageCache.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + C84972B0166564D000DB15D1 /* OGImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageCache.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C86E0BEA1667FE36006063B6 /* OGImageProcessing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OGImageProcessing.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - C86E0BEB1667FE36006063B6 /* OGImageProcessing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageProcessing.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + C86E0BEB1667FE36006063B6 /* OGImageProcessing.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageProcessing.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C86E0BF01667FF9E006063B6 /* OGImageProcessingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImageProcessingTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C871143F16640E510015A743 /* OGImageTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OGImageTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; C871144616640E510015A743 /* OGImageTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OGImageTests-Info.plist"; sourceTree = ""; }; @@ -119,9 +126,12 @@ C88272341663DBDF007D409F /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/OGViewController.xib; sourceTree = ""; }; C882723C1663E5B0007D409F /* OGImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OGImage.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; C882723D1663E5B0007D409F /* OGImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGImage.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + C88AE28D17C7EDD4002A34C7 /* OGImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OGImageView.h; sourceTree = ""; }; + C88AE28E17C7EDD4002A34C7 /* OGImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OGImageView.m; sourceTree = ""; }; C8975AE8166E09BB00C2D53A /* OGScaledImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OGScaledImage.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; C8975AE9166E09BB00C2D53A /* OGScaledImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OGScaledImage.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C8FADED8166D5DB100829179 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + F68541031A096AED001D6727 /* OGImageScaleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OGImageScaleTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -156,6 +166,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 18B84B7401884D0C718750AB /* Pods */ = { + isa = PBXGroup; + children = ( + 623F95686DC456957AC440BC /* Pods.debug.xcconfig */, + 613EC03F3D03DD7743B0B058 /* Pods.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; C80306271665269B00073395 /* Resources */ = { isa = PBXGroup; children = ( @@ -170,6 +189,7 @@ isa = PBXGroup; children = ( C8392EAC169CA1E100F0907A /* Origami.jpg */, + 9C46672018D39709008960BA /* moldex-logo.gif */, ); name = Resources; sourceTree = ""; @@ -184,6 +204,8 @@ C8392EAF169CA21700F0907A /* OGImageFileTests.m */, C87C645E16C46C70006217C9 /* OGImageIdempotentTests.m */, C86E0BF01667FF9E006063B6 /* OGImageProcessingTests.m */, + 9C46672218D39EA7008960BA /* OGImageProblematicProcessingTests.m */, + F68541031A096AED001D6727 /* OGImageScaleTests.m */, C808967F1694ED01009BE21D /* OGImageTestsAppDelegate.h */, C80896801694ED01009BE21D /* OGImageTestsAppDelegate.m */, ); @@ -211,7 +233,7 @@ C871144416640E510015A743 /* OGImageTests */, C88272171663DBDF007D409F /* Frameworks */, C88272151663DBDF007D409F /* Products */, - 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */, + 18B84B7401884D0C718750AB /* Pods */, ); sourceTree = ""; }; @@ -272,6 +294,8 @@ C882723B1663E570007D409F /* OGImage */ = { isa = PBXGroup; children = ( + C83BE57C16C5904600D82A1A /* __OGImage.h */, + C83BE57D16C5904600D82A1A /* __OGImage.m */, C80306321665421300073395 /* OGCachedImage.h */, C80306331665421300073395 /* OGCachedImage.m */, C882723C1663E5B0007D409F /* OGImage.h */, @@ -282,12 +306,12 @@ C8711464166417DE0015A743 /* OGImageLoader.m */, C86E0BEA1667FE36006063B6 /* OGImageProcessing.h */, C86E0BEB1667FE36006063B6 /* OGImageProcessing.m */, - C8975AE8166E09BB00C2D53A /* OGScaledImage.h */, - C8975AE9166E09BB00C2D53A /* OGScaledImage.m */, C808967A1694DFC0009BE21D /* OGImageRequest.h */, C808967B1694DFC0009BE21D /* OGImageRequest.m */, - C83BE57C16C5904600D82A1A /* __OGImage.h */, - C83BE57D16C5904600D82A1A /* __OGImage.m */, + C88AE28D17C7EDD4002A34C7 /* OGImageView.h */, + C88AE28E17C7EDD4002A34C7 /* OGImageView.m */, + C8975AE8166E09BB00C2D53A /* OGScaledImage.h */, + C8975AE9166E09BB00C2D53A /* OGScaledImage.m */, ); name = OGImage; path = ../../OGImage; @@ -338,7 +362,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = OG; - LastUpgradeCheck = 0450; + LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Origami Labs"; }; buildConfigurationList = C882720E1663DBDF007D409F /* Build configuration list for PBXProject "OGImageDemo" */; @@ -366,6 +390,7 @@ files = ( C871144916640E510015A743 /* InfoPlist.strings in Resources */, C871145116640E510015A743 /* Default.png in Resources */, + 9C46672118D39709008960BA /* moldex-logo.gif in Resources */, C871145316640E510015A743 /* Default@2x.png in Resources */, C871145516640E510015A743 /* Default-568h@2x.png in Resources */, C8392EAD169CA1E100F0907A /* Origami.jpg in Resources */, @@ -402,7 +427,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Pods-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -412,11 +437,13 @@ buildActionMask = 2147483647; files = ( C871144B16640E510015A743 /* main.m in Sources */, + 9C46672318D39EA7008960BA /* OGImageProblematicProcessingTests.m in Sources */, C871146116640F7D0015A743 /* OGImageAsyncTests.m in Sources */, C8711462166410CD0015A743 /* OGImage.m in Sources */, C8711466166417E20015A743 /* OGImageLoader.m in Sources */, C80306351665421300073395 /* OGCachedImage.m in Sources */, C86E0BED1667FE36006063B6 /* OGImageProcessing.m in Sources */, + F68541041A096AED001D6727 /* OGImageScaleTests.m in Sources */, C86E0BF11667FF9E006063B6 /* OGImageProcessingTests.m in Sources */, C86E0BF216680030006063B6 /* OGImageCache.m in Sources */, C8975AEB166E09BB00C2D53A /* OGScaledImage.m in Sources */, @@ -445,6 +472,7 @@ C8975AEA166E09BB00C2D53A /* OGScaledImage.m in Sources */, C808967C1694DFC0009BE21D /* OGImageRequest.m in Sources */, C83BE57E16C5904600D82A1A /* __OGImage.m in Sources */, + C88AE28F17C7EDD4002A34C7 /* OGImageView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -480,7 +508,7 @@ /* Begin XCBuildConfiguration section */ C871145616640E510015A743 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */; + baseConfigurationReference = 623F95686DC456957AC440BC /* Pods.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -493,7 +521,7 @@ }; C871145716640E510015A743 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */; + baseConfigurationReference = 613EC03F3D03DD7743B0B058 /* Pods.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -527,6 +555,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 6.0; + ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "-all_load", "-ObjC", @@ -565,7 +594,7 @@ }; C88272391663DBDF007D409F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */; + baseConfigurationReference = 623F95686DC456957AC440BC /* Pods.debug.xcconfig */; buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "OGImageDemo/OGImageDemo-Prefix.pch"; @@ -577,7 +606,7 @@ }; C882723A1663DBDF007D409F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */; + baseConfigurationReference = 613EC03F3D03DD7743B0B058 /* Pods.release.xcconfig */; buildSettings = { GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "OGImageDemo/OGImageDemo-Prefix.pch"; diff --git a/OGImageDemo/OGImageDemo/OGAppDelegate.m b/OGImageDemo/OGImageDemo/OGAppDelegate.m index 11ad283..5f3a376 100644 --- a/OGImageDemo/OGImageDemo/OGAppDelegate.m +++ b/OGImageDemo/OGImageDemo/OGAppDelegate.m @@ -18,7 +18,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[OGViewController alloc] initWithNibName:@"OGViewController" bundle:nil]; - self.window.rootViewController = self.viewController; + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self.viewController]; + self.window.rootViewController = navController; [self.window makeKeyAndVisible]; [self setupLogging]; return YES; diff --git a/OGImageDemo/OGImageDemo/OGImageTableViewCell.h b/OGImageDemo/OGImageDemo/OGImageTableViewCell.h index b6fd3ae..1a4c3bc 100644 --- a/OGImageDemo/OGImageDemo/OGImageTableViewCell.h +++ b/OGImageDemo/OGImageDemo/OGImageTableViewCell.h @@ -7,11 +7,10 @@ // #import - -@class OGScaledImage; +#import "OGImageView.h" @interface OGImageTableViewCell : UITableViewCell -@property (nonatomic, strong) OGScaledImage *image; +@property (nonatomic, readonly, strong) OGImageView *ogImageView; @end diff --git a/OGImageDemo/OGImageDemo/OGImageTableViewCell.m b/OGImageDemo/OGImageDemo/OGImageTableViewCell.m index 368fb79..f9096e2 100644 --- a/OGImageDemo/OGImageDemo/OGImageTableViewCell.m +++ b/OGImageDemo/OGImageDemo/OGImageTableViewCell.m @@ -7,15 +7,17 @@ // #import "OGImageTableViewCell.h" -#import "OGScaledImage.h" - -static NSString *KVOContext = @"OGImageTableViewCell observation"; @implementation OGImageTableViewCell - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { + OGImageView *tmp = [[OGImageView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.bounds.size.height, self.bounds.size.height)]; + tmp.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + [self.contentView addSubview:tmp]; + _ogImageView = tmp; + _ogImageView.clipsToBounds = YES; } return self; } @@ -24,43 +26,15 @@ - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; } -#pragma mark - KVO - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if( (void *)&KVOContext == context ) { - NSAssert(YES == [NSThread isMainThread], @"KVO fired on thread other than main..."); - if ([keyPath isEqualToString:@"scaledImage"]) { - self.imageView.image = self.image.scaledImage; - self.textLabel.text = [[self.image.url path] lastPathComponent]; - } else if ([keyPath isEqualToString:@"error"]) { - self.detailTextLabel.textColor = [UIColor redColor]; - self.detailTextLabel.text = [NSString stringWithFormat:NSLocalizedString(@"%@", @""), [self.image.error localizedDescription]]; - self.imageView.image = self.image.scaledImage; - [self setNeedsLayout]; - } +- (void)layoutSubviews { + [super layoutSubviews]; + // move the textLabel over to accomodate the ogImageView + CGRect f = self.textLabel.frame; + f.origin.x = self.ogImageView.bounds.size.width + 5.f; + if (self.bounds.size.width - 10.f < f.origin.x + f.size.width) { + f.size.width = self.bounds.size.width - 10.f - f.origin.x; } - else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -#pragma mark - Properties - -- (void)setImage:(OGScaledImage *)image { - self.detailTextLabel.text = @""; - /* - * When the cell's image is set, we want to first make sure we're no longer listening - * for any KVO notifications on the cell's previous image. - */ - [_image removeObserver:self context:&KVOContext]; - _image = image; - [_image addObserver:self context:&KVOContext]; - self.textLabel.text = [[self.image.url path] lastPathComponent]; - self.imageView.image = _image.scaledImage; -} - -- (void)dealloc { - [_image removeObserver:self context:&KVOContext]; + self.textLabel.frame = f; } @end diff --git a/OGImageDemo/OGImageDemo/OGViewController.m b/OGImageDemo/OGImageDemo/OGViewController.m index 2fd6500..b760ec3 100644 --- a/OGImageDemo/OGImageDemo/OGViewController.m +++ b/OGImageDemo/OGImageDemo/OGViewController.m @@ -7,8 +7,6 @@ // #import "OGViewController.h" -#import "OGScaledImage.h" -#import "OGImageCache.h" #import "OGImageTableViewCell.h" @interface OGViewController () @@ -21,12 +19,12 @@ @implementation OGViewController { - (void)viewDidLoad { [super viewDidLoad]; + self.title = NSLocalizedString(@"OGImageDemo", @""); [self loadJSON]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. } - (void)loadJSON { @@ -62,11 +60,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N OGImageTableViewCell *cell = (OGImageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:OGImageCellIdentifier]; if (nil == cell) { cell = [[OGImageTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:OGImageCellIdentifier]; + cell.ogImageView.contentMode = UIViewContentModeScaleAspectFill; } NSURL *imageURL = [NSURL URLWithString:_urls[indexPath.row]]; - CGFloat imageSide = self.tableView.rowHeight; - OGScaledImage *image = [[OGScaledImage alloc] initWithURL:imageURL size:CGSizeMake(imageSide, imageSide) cornerRadius:0.f method:OGImageProcessingScale_AspectFill key:nil placeholderImage:[UIImage imageNamed:@"placeholder"]]; - cell.image = image; + [cell.ogImageView setImageURL:imageURL placeholder:[UIImage imageNamed:@"placeholder"]]; + cell.textLabel.text = [imageURL lastPathComponent]; return cell; } diff --git a/OGImageDemo/OGImageTests/OGImageAssetsLibraryTests.m b/OGImageDemo/OGImageTests/OGImageAssetsLibraryTests.m index 0a71a11..71098e9 100644 --- a/OGImageDemo/OGImageTests/OGImageAssetsLibraryTests.m +++ b/OGImageDemo/OGImageTests/OGImageAssetsLibraryTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2013 Origami Labs. All rights reserved. // -#import "GHAsyncTestCase.h" +#import #import "OGCachedImage.h" #import "OGImageCache.h" diff --git a/OGImageDemo/OGImageTests/OGImageAsyncTests.m b/OGImageDemo/OGImageTests/OGImageAsyncTests.m index 3d64c9e..619f65b 100644 --- a/OGImageDemo/OGImageTests/OGImageAsyncTests.m +++ b/OGImageDemo/OGImageTests/OGImageAsyncTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2012 Origami Labs, Inc.. All rights reserved. // -#import +#import #import "OGImage.h" #import "OGImageLoader.h" diff --git a/OGImageDemo/OGImageTests/OGImageFileTests.m b/OGImageDemo/OGImageTests/OGImageFileTests.m index 32a6e64..889b31f 100644 --- a/OGImageDemo/OGImageTests/OGImageFileTests.m +++ b/OGImageDemo/OGImageTests/OGImageFileTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2013 Origami Labs. All rights reserved. // -#import "GHAsyncTestCase.h" +#import #import "OGCachedImage.h" #import "OGImageCache.h" diff --git a/OGImageDemo/OGImageTests/OGImageIdempotentTests.m b/OGImageDemo/OGImageTests/OGImageIdempotentTests.m index 2ffe697..1854416 100644 --- a/OGImageDemo/OGImageTests/OGImageIdempotentTests.m +++ b/OGImageDemo/OGImageTests/OGImageIdempotentTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2013 Origami Labs. All rights reserved. // -#import "GHAsyncTestCase.h" +#import #import "OGImage.h" static NSString *KVOContext = @"OGImageIdempotentTests observation"; diff --git a/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m new file mode 100644 index 0000000..445a365 --- /dev/null +++ b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m @@ -0,0 +1,123 @@ +// +// OGImageProblematicProcessingTests.m +// OGImageDemo +// +// Created by Sixten Otto on 3/14/14. +// Copyright (c) 2014 Sixten Otto. All rights reserved. +// + +#import +#import +#import "OGImageProcessing.h" +#import "OGScaledImage.h" +#import "OGImageCache.h" + +extern OSStatus UIImageToVImageBuffer(UIImage *image, vImage_Buffer *buffer, CGImageAlphaInfo alphaInfo); + +static NSString *KVOContext = @"OGImageProblematicProcessingTests observation"; +static const CGSize TEST_SCALE_SIZE = {100.f, 20.f}; + +@interface NoOpAssertionHandler : NSAssertionHandler +@end + + +@interface OGImageProblematicProcessingTests : GHAsyncTestCase +@end + +@implementation OGImageProblematicProcessingTests + +- (void)setUp { + // make sure we get the image from the network + [[OGImageCache shared] purgeCache:YES]; +} + +- (void)tearDown { + // clean up the in-memory and disk cache when we're done + [[OGImageCache shared] purgeCache:YES]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if( (void *)&KVOContext == context ) { + NSAssert(YES == [NSThread isMainThread], @"Expected `observeValueForKeyPath` to only be called on main thread"); + if( [keyPath isEqualToString:@"scaledImage"] ) { + OGScaledImage *image = (OGScaledImage *)object; + GHTestLog(@"Scaled image loaded: %@ : %@", image.image, NSStringFromCGSize(image.scaledImage.size)); + if( nil == image ) { + [self notify:kGHUnitWaitStatusFailure]; + } + else { + [self notify:kGHUnitWaitStatusSuccess]; + } + return; + } + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)testScalingGif +{ + [self prepare]; + NSURL *url = [[NSBundle mainBundle] URLForResource:@"moldex-logo" withExtension:@"gif"]; + OGScaledImage *image = [[OGScaledImage alloc] initWithURL:url size:TEST_SCALE_SIZE key:nil]; + [image addObserver:self context:&KVOContext]; + [self waitForStatus:kGHUnitWaitStatusSuccess timeout:10.]; + [image removeObserver:self context:&KVOContext]; +} + +- (void)testCachingNil +{ + NSURL *url = [[NSBundle mainBundle] URLForResource:@"moldex-logo" withExtension:@"gif"]; + __OGImage *image = [[__OGImage alloc] initWithDataAtURL:url]; + GHAssertNotNil(image, @"Couldn't decode test image"); + + // make sure that the test isn't interrupted by assert failure + NSAssertionHandler *oldHandler = [[[NSThread currentThread] threadDictionary] valueForKey:NSAssertionHandlerKey]; + NSAssertionHandler *tempHandler = [NoOpAssertionHandler new]; + [[[NSThread currentThread] threadDictionary] setValue:tempHandler forKey:NSAssertionHandlerKey]; + + GHAssertNoThrowSpecific([[OGImageCache shared] setImage:nil forKey:@"foo"], NSException, NSInvalidArgumentException, @"Attempting to insert a nil image should not throw"); + GHAssertNoThrowSpecific([[OGImageCache shared] setImage:image forKey:nil], NSException, NSInvalidArgumentException, @"Attempting to insert with a nil key should not throw"); + + [[[NSThread currentThread] threadDictionary] setValue:oldHandler forKey:NSAssertionHandlerKey]; +} + +- (void)testConvertingBadAlpha_Last +{ + NSString *path = [[NSBundle mainBundle] pathForResource:@"moldex-logo" ofType:@"gif"]; + UIImage *image = [[UIImage alloc] initWithContentsOfFile:path]; + + vImage_Buffer vBuffer; + OSStatus result = UIImageToVImageBuffer(image, &vBuffer, kCGImageAlphaLast); + GHAssertErr(OGImageProcessingError, result, @"Operation should report failure"); + GHAssertNULL(vBuffer.data, @"Buffer should have NULL data pointer"); +} + +- (void)testConvertingBadAlpha_First +{ + NSString *path = [[NSBundle mainBundle] pathForResource:@"moldex-logo" ofType:@"gif"]; + UIImage *image = [[UIImage alloc] initWithContentsOfFile:path]; + + vImage_Buffer vBuffer; + OSStatus result = UIImageToVImageBuffer(image, &vBuffer, kCGImageAlphaFirst); + GHAssertErr(OGImageProcessingError, result, @"Operation should report failure"); + GHAssertNULL(vBuffer.data, @"Buffer should have NULL data pointer"); +} + +@end + +@implementation NoOpAssertionHandler + +- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... +{ + NSLog(@"Assertion failure (ignored): %@ for object %@ in %@#%li", NSStringFromSelector(selector), object, fileName, (long)line); +} + +- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... +{ + NSLog(@"Assertion failure (ignored): %@ in %@#%li", functionName, fileName, (long)line); +} + +@end + diff --git a/OGImageDemo/OGImageTests/OGImageProcessingTests.m b/OGImageDemo/OGImageTests/OGImageProcessingTests.m index fa982e0..3f2bbe1 100644 --- a/OGImageDemo/OGImageTests/OGImageProcessingTests.m +++ b/OGImageDemo/OGImageTests/OGImageProcessingTests.m @@ -6,7 +6,7 @@ // Copyright (c) 2012 Origami Labs, Inc.. All rights reserved. // -#import +#import #import "OGImageProcessing.h" #import "OGScaledImage.h" #import "OGImageCache.h" diff --git a/OGImageDemo/OGImageTests/OGImageScaleTests.m b/OGImageDemo/OGImageTests/OGImageScaleTests.m new file mode 100644 index 0000000..e0b3ecc --- /dev/null +++ b/OGImageDemo/OGImageTests/OGImageScaleTests.m @@ -0,0 +1,104 @@ +// +// OGImageScaleTests.m +// OGImageDemo +// +// Created by Sixten Otto on 11/4/14. +// Copyright (c) 2014 Origami Labs. All rights reserved. +// + +#import +#import "__OGImage.h" +//#import "OGImageCache.h" + +@interface OGImageScaleTests : GHAsyncTestCase + +@property (strong, nonatomic) NSURL *destinationDirectoryURL; + +@end + +@implementation OGImageScaleTests + +- (void)setUp { + NSURL *tempDir = [[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES] URLByAppendingPathComponent:@"OGImageScaleTests" isDirectory:YES]; + if( ![[NSFileManager defaultManager] fileExistsAtPath:[tempDir path]] ) { + [[NSFileManager defaultManager] createDirectoryAtURL:tempDir withIntermediateDirectories:NO attributes:nil error:NULL]; + } + self.destinationDirectoryURL = tempDir; + + //[[OGImageCache shared] purgeCache:YES]; +} + +- (void)tearDown { + [[NSFileManager defaultManager] removeItemAtURL:self.destinationDirectoryURL error:NULL]; + //[[OGImageCache shared] purgeCache:YES]; +} + +- (UIImage *)newTestImageAtScale:(float)scale +{ + CGSize size = CGSizeMake(80, 17); + CGRect bounds = (CGRect){.origin=CGPointZero, .size=size}; + UIGraphicsBeginImageContextWithOptions(size, YES, scale); + + [[UIColor whiteColor] setFill]; + UIRectFill(bounds); + + CGFloat r = (arc4random_uniform(101) / 100.f), + g = (arc4random_uniform(101) / 100.f), + b = (arc4random_uniform(101) / 100.f); + [[UIColor colorWithRed:r green:g blue:b alpha:0.5f] setFill]; + UIRectFill(bounds); + + [@"Lorem ipsum dolor sit amet" drawInRect:bounds withFont:[UIFont systemFontOfSize:15]]; + + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return result; +} + +- (void)test1xImagesHaveNoExtension { + UIImage *testImg = [self newTestImageAtScale:1.0f]; + __OGImage *img = [[__OGImage alloc] initWithCGImage:testImg.CGImage scale:testImg.scale orientation:UIImageOrientationUp]; + NSURL *fileURL = [self.destinationDirectoryURL URLByAppendingPathComponent:@"one_echs_image.png"]; + + BOOL success = [img writeToURL:fileURL error:NULL]; + GHAssertTrue(success, @"Should successfully write to file."); + GHAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]], @"File should exist with unmodified name"); +} + +- (void)testRetinaImagesHaveResolutionExtensions { + for( CGFloat s = 2.0f; s < 6.0f; ++s ) { + UIImage *testImg = [self newTestImageAtScale:s]; + __OGImage *img = [[__OGImage alloc] initWithCGImage:testImg.CGImage scale:s orientation:UIImageOrientationUp]; + NSURL *fileURL = [self.destinationDirectoryURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + NSURL *expectedFileURL = [fileURL URLByAppendingPathExtension:[NSString stringWithFormat:@"@%lix", (long)s]]; + + BOOL success = [img writeToURL:fileURL error:NULL]; + GHAssertTrue(success, @"Should successfully write to file."); + GHAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]], @"File should not exist with unmodified name at scale %.0f", s); + GHAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:[expectedFileURL path]], @"File should exist with modified name at scale %.0f", s); + } +} + +- (void)testImagesWithNoExtensionLoadedAs1x { + UIImage *testImg = [self newTestImageAtScale:1.0f]; + NSURL *fileURL = [self.destinationDirectoryURL URLByAppendingPathComponent:@"one_echs_image.png"]; + [UIImagePNGRepresentation(testImg) writeToURL:fileURL atomically:YES]; + + __OGImage *img = [[__OGImage alloc] initWithDataAtURL:fileURL]; + GHAssertNotNil(img, @"Should load the image."); + GHAssertEquals((CGFloat)1.0f, img.scale, @"Loaded image should be 1x"); +} + +- (void)testImagesWithResolutionExtensionsLoadedWithScale { + for( CGFloat s = 2.0f; s < 6.0f; ++s ) { + UIImage *testImg = [self newTestImageAtScale:s]; + NSURL *fileURL = [self.destinationDirectoryURL URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + [UIImagePNGRepresentation(testImg) writeToURL:[fileURL URLByAppendingPathExtension:[NSString stringWithFormat:@"@%lix", (long)s]] atomically:YES]; + + __OGImage *img = [[__OGImage alloc] initWithDataAtURL:fileURL]; + GHAssertNotNil(img, @"Should load the image at scale %.0f", s); + GHAssertEquals(s, img.scale, @"Loaded image should match scale %.0f", s); + } +} + +@end diff --git a/OGImageDemo/OGImageTests/OGImageTestsAppDelegate.h b/OGImageDemo/OGImageTests/OGImageTestsAppDelegate.h index 4df5836..892bab6 100644 --- a/OGImageDemo/OGImageTests/OGImageTestsAppDelegate.h +++ b/OGImageDemo/OGImageTests/OGImageTestsAppDelegate.h @@ -6,7 +6,7 @@ // Copyright (c) 2013 Origami Labs. All rights reserved. // -#import "GHUnitIOSAppDelegate.h" +#import @interface OGImageTestsAppDelegate : GHUnitIOSAppDelegate diff --git a/OGImageDemo/OGImageTests/moldex-logo.gif b/OGImageDemo/OGImageTests/moldex-logo.gif new file mode 100644 index 0000000..ed96162 Binary files /dev/null and b/OGImageDemo/OGImageTests/moldex-logo.gif differ diff --git a/OGImageDemo/Podfile b/OGImageDemo/Podfile index a7937e3..bac8ff6 100644 --- a/OGImageDemo/Podfile +++ b/OGImageDemo/Podfile @@ -1,4 +1,6 @@ -platform :ios -pod 'GHUnitIOS', '~> 0.5.5' +platform :ios, '6.0' +source 'https://github.com/CocoaPods/Specs.git' + +pod 'GHUnit', '~> 0.5.8' pod 'CocoaLumberjack', '~> 1.6' diff --git a/README.md b/README.md index 106c954..e00f28e 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,28 @@ more information. The idea behind `OGImage` is to encapsulate best practices for loading images over HTTP in a simple, extensible interface. +### OGImageView + +If all you need is to load an image and display it in a `UIImageView`, check out +`OGImageView`, a `UIImageView` subclass that adds a single method: + +```objc + +[cell.ogImageView setImageURL:someURL placeholder:[UIImage imageNamed:@"placeholder"]]; + +``` + +This one call will handle fetching the image at `someURL` (regardless of whether it's a network, file +or even `assets-library:` url), scaling it to the `OGImageView`s `bounds.size` obeying `contentMode` *and* +caching it in memory and on-disk, and swapping out your placeholder image with the new image. + +Furthermore, if `setImageURL:placeholder:` is called on an existing `OGImageView` instance (e.g., when +its containing `UITableViewCell` is recycled) `OGImageView` will behave as you'd expect: It loses interest +in the previously requested URL and the current URL is given first priority for fetching/processing. + ### Philosophy -* The default use case should be *ridiculously* simple to execute. In OGImage, +* The default use case should be *ridiculously* simple to execute (see [OGImageView](#OGImageView)). In OGImage, you can load, cache, and scale an image with the following call: ```objc