From cf17a504ac8fb4a8f09b88e77dc1dfee5f30c348 Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Sun, 11 Aug 2013 09:54:49 -0700 Subject: [PATCH 01/14] remove redundant `CGImageSoureCopyPropertiesAtIndex` --- OGImage/__OGImage.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OGImage/__OGImage.m b/OGImage/__OGImage.m index 54e460f..08451db 100644 --- a/OGImage/__OGImage.m +++ b/OGImage/__OGImage.m @@ -89,8 +89,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); } From 953fbfc42366b7b2706d71c216213c07293288a3 Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 14:26:31 -0700 Subject: [PATCH 02/14] adds scaling method to key. This fixes a subtle bug where if you scaled an image using one scale method, then requested the same image at the same size but with a different scale method, you'd get a hit from the cache for the earlier image (i.e., the wrong scale method) --- OGImage/OGScaledImage.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OGImage/OGScaledImage.m b/OGImage/OGScaledImage.m index 9424332..f7d9855 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-%d", origKey, size.width, size.height, cornerRadius, 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; From c0af5934e6caa9bed8d6bec88ed388cb7f4b13c1 Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 14:44:51 -0700 Subject: [PATCH 03/14] adds `OGImageView` `OGImageView` is a subclass of `UIImageView` that encapsulates all the behavior of `OGImage` (specifically, `OGScaledImage`) to handle the most common case of loading an image from a URL and displaying it. --- OGImage/OGImageView.h | 32 ++++++++++++++++++++++++++ OGImage/OGImageView.m | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 OGImage/OGImageView.h create mode 100644 OGImage/OGImageView.m 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 From 800daf53eb1e1195e8fcf8f74e3dd7432276fc0c Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 14:45:23 -0700 Subject: [PATCH 04/14] updates demo for `OGImageView` and iOS 7. --- .../OGImageDemo.xcodeproj/project.pbxproj | 14 +++-- OGImageDemo/OGImageDemo/OGAppDelegate.m | 3 +- .../OGImageDemo/OGImageTableViewCell.h | 5 +- .../OGImageDemo/OGImageTableViewCell.m | 53 +++++-------------- OGImageDemo/OGImageDemo/OGViewController.m | 10 ++-- 5 files changed, 31 insertions(+), 54 deletions(-) diff --git a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj index 5856587..eba7c17 100644 --- a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj +++ b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 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 */; }; @@ -119,6 +120,8 @@ 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; }; @@ -272,6 +275,8 @@ C882723B1663E570007D409F /* OGImage */ = { isa = PBXGroup; children = ( + C83BE57C16C5904600D82A1A /* __OGImage.h */, + C83BE57D16C5904600D82A1A /* __OGImage.m */, C80306321665421300073395 /* OGCachedImage.h */, C80306331665421300073395 /* OGCachedImage.m */, C882723C1663E5B0007D409F /* OGImage.h */, @@ -282,12 +287,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; @@ -445,6 +450,7 @@ C8975AEA166E09BB00C2D53A /* OGScaledImage.m in Sources */, C808967C1694DFC0009BE21D /* OGImageRequest.m in Sources */, C83BE57E16C5904600D82A1A /* __OGImage.m in Sources */, + C88AE28F17C7EDD4002A34C7 /* OGImageView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OGImageDemo/OGImageDemo/OGAppDelegate.m b/OGImageDemo/OGImageDemo/OGAppDelegate.m index 54781eb..0b9ca10 100644 --- a/OGImageDemo/OGImageDemo/OGAppDelegate.m +++ b/OGImageDemo/OGImageDemo/OGAppDelegate.m @@ -17,7 +17,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 371c701..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,44 +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]; - self.detailTextLabel.text = [NSString stringWithFormat:@"%.2f", self.image.loadTime]; - } else if ([keyPath isEqualToString:@"error"]) { - - } +- (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 { - /* - * 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 forKeyPath:@"error" context:&KVOContext]; - [_image removeObserver:self forKeyPath:@"scaledImage" context:&KVOContext]; - _image = image; - self.imageView.image = _image.scaledImage; - self.textLabel.text = [[self.image.url path] lastPathComponent]; - self.detailTextLabel.text = NSLocalizedString(@"Loading", @""); - [_image addObserver:self forKeyPath:@"error" options:NSKeyValueObservingOptionNew context:&KVOContext]; - [_image addObserver:self forKeyPath:@"scaledImage" options:NSKeyValueObservingOptionNew context:&KVOContext]; -} - -- (void)dealloc { - [_image removeObserver:self forKeyPath:@"error" context:&KVOContext]; - [_image removeObserver:self forKeyPath:@"scaledImage" context:&KVOContext]; + self.textLabel.frame = f; } @end diff --git a/OGImageDemo/OGImageDemo/OGViewController.m b/OGImageDemo/OGImageDemo/OGViewController.m index 6e97816..b760ec3 100644 --- a/OGImageDemo/OGImageDemo/OGViewController.m +++ b/OGImageDemo/OGImageDemo/OGViewController.m @@ -7,7 +7,6 @@ // #import "OGViewController.h" -#import "OGScaledImage.h" #import "OGImageTableViewCell.h" @interface OGViewController () @@ -20,13 +19,12 @@ @implementation OGViewController { - (void)viewDidLoad { [super viewDidLoad]; + self.title = NSLocalizedString(@"OGImageDemo", @""); [self loadJSON]; - // Do any additional setup after loading the view, typically from a nib. } - (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; } From 0edf217f0843dd38e208307d5dd37a80903380bf Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 15:02:19 -0700 Subject: [PATCH 05/14] Updates readme with description of `OGImageView` --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 106c954..c53da14 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 From 1b6f62c817db40ec7b06afee452a4fd060eda533 Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 15:05:32 -0700 Subject: [PATCH 06/14] readme link formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c53da14..f64c475 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ in the previously requested URL and the current URL is given first priority for ### Philosophy -* The default use case should be *ridiculously* simple to execute (see `[OGImageView]`(#OGImageView)). 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 From a82cbcfece28f7505c2d55187eded8e35147490a Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Fri, 23 Aug 2013 15:06:47 -0700 Subject: [PATCH 07/14] adds mo' readme fixes. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f64c475..ac22e72 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ in the previously requested URL and the current URL is given first priority for ### Philosophy -* The default use case should be *ridiculously* simple to execute (see `[OGImageView](#OGImageView)`). 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 From 193aba98f7f0f61d44922b0c066a38da6e523581 Mon Sep 17 00:00:00 2001 From: Art Gillespie Date: Tue, 27 Aug 2013 09:10:13 -0700 Subject: [PATCH 08/14] Minor formatting in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac22e72..e00f28e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ If all you need is to load an image and display it in a `UIImageView`, check out ```objc - [cell.ogImageView setImageURL:someURL placeholder:[UIImage imageNamed:@"placeholder"]]; +[cell.ogImageView setImageURL:someURL placeholder:[UIImage imageNamed:@"placeholder"]]; ``` From ec8286738f82522fef3b3496588dc52a2108d57a Mon Sep 17 00:00:00 2001 From: Sixten Otto Date: Fri, 14 Mar 2014 15:54:01 -0700 Subject: [PATCH 09/14] test coverage for some issues that have cropped up in our apps --- .../OGImageDemo.xcodeproj/project.pbxproj | 12 +- .../OGImageProblematicProcessingTests.m | 123 ++++++++++++++++++ OGImageDemo/OGImageTests/moldex-logo.gif | Bin 0 -> 2101 bytes 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m create mode 100644 OGImageDemo/OGImageTests/moldex-logo.gif diff --git a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj index eba7c17..c8d8cf7 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 */; }; @@ -66,6 +68,8 @@ /* Begin PBXFileReference section */ 467479B3192A45B8984313B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = SOURCE_ROOT; }; 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; }; @@ -86,9 +90,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 = ""; }; @@ -173,6 +177,7 @@ isa = PBXGroup; children = ( C8392EAC169CA1E100F0907A /* Origami.jpg */, + 9C46672018D39709008960BA /* moldex-logo.gif */, ); name = Resources; sourceTree = ""; @@ -187,6 +192,7 @@ C8392EAF169CA21700F0907A /* OGImageFileTests.m */, C87C645E16C46C70006217C9 /* OGImageIdempotentTests.m */, C86E0BF01667FF9E006063B6 /* OGImageProcessingTests.m */, + 9C46672218D39EA7008960BA /* OGImageProblematicProcessingTests.m */, C808967F1694ED01009BE21D /* OGImageTestsAppDelegate.h */, C80896801694ED01009BE21D /* OGImageTestsAppDelegate.m */, ); @@ -371,6 +377,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 */, @@ -417,6 +424,7 @@ 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 */, diff --git a/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m new file mode 100644 index 0000000..0b2b0da --- /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 %@#%i", NSStringFromSelector(selector), object, fileName, line); +} + +- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... +{ + NSLog(@"Assertion failure (ignored): %@ in %@#%i", functionName, fileName, line); +} + +@end + diff --git a/OGImageDemo/OGImageTests/moldex-logo.gif b/OGImageDemo/OGImageTests/moldex-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ed96162bb918f18c8a3506e0542024c6f34b3802 GIT binary patch literal 2101 zcmV-52+H?INk%w1VbTC70M!5hH(Z)HU7J2)o-h@QiYqQ#J<$C0JT zm8r^_t<0XV&Y-c+q_xqfxYMh<)UdzTvB1}~!r8dR+r7!&z{}sl&EUq+;>*?L&)DbC z*yq#R>Db@v+~e)wFxCG@%HfZ_ww}k_W1h#{{8>}000000000000000 z00000A^8Le0024wEC2ui0MY;`000L6z?^VMEEbV{vSuTT!sB;N2z=2TPSI@>Y@w!CHXW`PzUCnIL2GvG559aP~xkfV=tncLp2pq1R2=3^> zNlc8hN3hm75c{Ca*Z{4d!*d0|Apuc*ed1ima--~kK&C?aI-6lvgvt^@WEFu*}00t5>VHV_d}Gi3`S;)t#+ z0;1~B1PW@JAhN=1xtmLLesvetfQlqHD1=<=(7*#DHy)V1w?c@B2OSoK8A4&Ny@*EW z5j{~P0|C0U2uRFP)+SDth9SQ(&{rM=0}3=3G+>Gdhq>-Z6QR+WNMWKDiri>`vTlc| zphdqyaFufA1Zac(hJ3(5$sBoe>n7-Sh>GFG6DE265P<{fE{2;kaKSlh-TlH;b{9P0 z@LsjwDJYPkNa4`S2_hhwqlU$I27dN&kiOu7csYci-96pa$3T3wR5PAA<>mBSIx3`) zf(j>SfPn+`Bo&Vc1;F?J&H=0}AmWH5j+g*a_+fB^1;Z6^n?n7KMuKYt!c-s!HG)zA zFDMdJzzQ$?)*xo~#Bc--EMTCW0qx~5LtTKKFvATt++gLESK45M3=i~Z&H)G7V*mtF z&@dE}L)>s*N@T(?U`-T=P(leDVCdy`5;8jkL@s0|Si0!59)ZNT6M?z|~5Gspg=HLRpLP6iBn?M@3^ z5b0*a!u9P?LzdeAClqSBieHn6oHP^yoG!8LOR7D&)_Jk9y8yDI@@bE=NNm9Cb{Euu z<-#}M091z}Ax4E_+>)8X!T45d?{^U^I|Q@k5PZbNbj?>{i4jD>O-kxe5y5Z`ytBX^ z7^jqNnQL||@H-+K5d*-G#iYhdJu7i;ISZ_iGr23@qeab|l_tf}52afK$g~tqM4|}x z>VOogig_p%H}I#P4OE|lp9|D+D|HlU^W?D*Si_;UN#lxE+!RYA?J%OlEs#S892c}e zTu{sMHWYFDICl_c<7V+*7X~q zQMv>SFV#E$`?%cY5CgZjl}=R3QqcaD)yu96g$IR;1R^m62+laK2HZSF*aQmErBZgU1u%@75cH+N4a{%|EclnZQp3RpBB2HqXb}k9Bqj>{ z%?kM27rd6B7n=;LY?dnk3WQ)ENsYz1_R?}FAE9=@cVt zM8H8dQ$r>s=qWW*9Ncga(10S8Kzq>}f-HmPz9ATFm#nOVC^fUeByi?m{kZ@P0d_Q zx}6Xd;h*kcKuKHx0hgKs0R@l%Mge#*hQ>7kVm-hG>Zk-8WI!nAY?%=nccKQ^zzCTl z&s|>$6YgaYKb*n=2+)zEn6T6cGAPU{lbVC2&Jz?2*Z}vS0D}y)<*{$b6%s1PoSf!> zMejhsZa Date: Fri, 14 Mar 2014 15:55:03 -0700 Subject: [PATCH 10/14] when someone tries to cache an empty image/key, don't crash in production --- OGImage/OGImageCache.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OGImage/OGImageCache.m b/OGImage/OGImageCache.m index 781935d..bc33a21 100644 --- a/OGImage/OGImageCache.m +++ b/OGImage/OGImageCache.m @@ -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]; From 6f0de0e9140a0d8b7ae7f092743f2e8f7d5879b8 Mon Sep 17 00:00:00 2001 From: Sixten Otto Date: Fri, 14 Mar 2014 15:56:37 -0700 Subject: [PATCH 11/14] handle invalid alpha options; creating bitmap context may fail --- OGImage/OGImageProcessing.m | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) 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; From 0b5b96c8c89083e279a7d2b1950cafff8390120b Mon Sep 17 00:00:00 2001 From: Sixten Otto Date: Tue, 4 Nov 2014 12:52:33 -0700 Subject: [PATCH 12/14] update config for latest CocoaPods, update dependencies --- .../OGImageDemo.xcodeproj/project.pbxproj | 27 +++++++++++++------ .../OGImageTests/OGImageAssetsLibraryTests.m | 2 +- OGImageDemo/OGImageTests/OGImageAsyncTests.m | 2 +- OGImageDemo/OGImageTests/OGImageFileTests.m | 2 +- .../OGImageTests/OGImageIdempotentTests.m | 2 +- .../OGImageProblematicProcessingTests.m | 2 +- .../OGImageTests/OGImageProcessingTests.m | 2 +- .../OGImageTests/OGImageTestsAppDelegate.h | 2 +- OGImageDemo/Podfile | 6 +++-- 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj index c8d8cf7..f252917 100644 --- a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj +++ b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj @@ -68,9 +68,10 @@ /* Begin PBXFileReference section */ 467479B3192A45B8984313B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; - 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.xcconfig; path = Pods/Pods.xcconfig; sourceTree = SOURCE_ROOT; }; 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; }; @@ -163,6 +164,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 = ( @@ -220,7 +230,7 @@ C871144416640E510015A743 /* OGImageTests */, C88272171663DBDF007D409F /* Frameworks */, C88272151663DBDF007D409F /* Products */, - 9F932054786D4C9BA76B5EBE /* Pods.xcconfig */, + 18B84B7401884D0C718750AB /* Pods */, ); sourceTree = ""; }; @@ -349,7 +359,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = OG; - LastUpgradeCheck = 0450; + LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Origami Labs"; }; buildConfigurationList = C882720E1663DBDF007D409F /* Build configuration list for PBXProject "OGImageDemo" */; @@ -414,7 +424,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 */ @@ -494,7 +504,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; @@ -507,7 +517,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; @@ -541,6 +551,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", @@ -579,7 +590,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"; @@ -591,7 +602,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/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 index 0b2b0da..e4dbdc5 100644 --- a/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m +++ b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m @@ -7,7 +7,7 @@ // #import -#import +#import #import "OGImageProcessing.h" #import "OGScaledImage.h" #import "OGImageCache.h" 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/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/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' From 8b9984a95e942f665e300be5545dbd671012c6ca Mon Sep 17 00:00:00 2001 From: Sixten Otto Date: Tue, 4 Nov 2014 13:08:21 -0700 Subject: [PATCH 13/14] correct some 64-bit warnings --- OGImage/OGImageCache.m | 2 +- OGImage/OGImageRequest.m | 2 +- OGImage/OGScaledImage.m | 2 +- OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OGImage/OGImageCache.m b/OGImage/OGImageCache.m index bc33a21..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]]; 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/OGScaledImage.m b/OGImage/OGScaledImage.m index f7d9855..7ab6e12 100644 --- a/OGImage/OGScaledImage.m +++ b/OGImage/OGScaledImage.m @@ -9,7 +9,7 @@ #import "OGImageCache.h" NSString *OGKeyWithSize(NSString *origKey, CGSize size, CGFloat cornerRadius, OGImageProcessingScaleMethod method) { - return [NSString stringWithFormat:@"%@-%f-%f-%f-%d", origKey, size.width, size.height, cornerRadius, method]; + return [NSString stringWithFormat:@"%@-%f-%f-%f-%ld", origKey, size.width, size.height, cornerRadius, (long)method]; } @implementation OGScaledImage { diff --git a/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m index e4dbdc5..445a365 100644 --- a/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m +++ b/OGImageDemo/OGImageTests/OGImageProblematicProcessingTests.m @@ -111,12 +111,12 @@ @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 %@#%i", NSStringFromSelector(selector), object, fileName, line); + 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 %@#%i", functionName, fileName, line); + NSLog(@"Assertion failure (ignored): %@ in %@#%li", functionName, fileName, (long)line); } @end From 7ba6e16483fda3a055e6bc6667f36c0477d19b39 Mon Sep 17 00:00:00 2001 From: Sixten Otto Date: Tue, 4 Nov 2014 16:04:33 -0700 Subject: [PATCH 14/14] handle retina resolutions >2x --- OGImage/__OGImage.h | 15 +-- OGImage/__OGImage.m | 34 +++++- .../OGImageDemo.xcodeproj/project.pbxproj | 4 + OGImageDemo/OGImageTests/OGImageScaleTests.m | 104 ++++++++++++++++++ 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 OGImageDemo/OGImageTests/OGImageScaleTests.m 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 08451db..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]; @@ -114,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 f252917..bb18248 100644 --- a/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj +++ b/OGImageDemo/OGImageDemo.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ 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 */ @@ -130,6 +131,7 @@ 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 */ @@ -203,6 +205,7 @@ C87C645E16C46C70006217C9 /* OGImageIdempotentTests.m */, C86E0BF01667FF9E006063B6 /* OGImageProcessingTests.m */, 9C46672218D39EA7008960BA /* OGImageProblematicProcessingTests.m */, + F68541031A096AED001D6727 /* OGImageScaleTests.m */, C808967F1694ED01009BE21D /* OGImageTestsAppDelegate.h */, C80896801694ED01009BE21D /* OGImageTestsAppDelegate.m */, ); @@ -440,6 +443,7 @@ 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 */, 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