Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
417 lines (361 sloc) 11.3 KB
//
// NYXProgressiveImageView.m
// NYXImagesKit
//
// Created by @Nyx0uf on 13/01/12.
// Copyright 2012 Nyx0uf. All rights reserved.
// www.cocoaintheshell.com
// Caching stuff by raphaelp
//
#import "NYXProgressiveImageView.h"
#import "NYXImagesHelper.h"
//#import "UIImage+Saving.h"
#import <ImageIO/ImageIO.h>
#import <CommonCrypto/CommonDigest.h>
#define kNyxDefaultCacheTimeValue 604800.0f // 7 days
#define kNyxDefaultTimeoutValue 10.0f
typedef struct
{
unsigned int delegateImageDidLoadWithImage:1;
unsigned int delegateImageDownloadCompletedWithImage:1;
unsigned int delegateImageDownloadFailedWithData:1;
} NyxDelegateFlags;
@interface NYXProgressiveImageView()
-(void)initializeAttributes;
-(CGImageRef)createTransitoryImage:(CGImageRef)partialImage CF_RETURNS_RETAINED;
+(NSString*)cacheDirectoryAddress;
-(NSString*)cachedImageSystemName;
-(void)resetCache;
+(UIImageOrientation)exifOrientationToiOSOrientation:(int)exifOrientation;
@end
@implementation NYXProgressiveImageView
{
/// Image download connection
NSURLConnection* _connection;
/// Downloaded data
NSMutableData* _dataTemp;
/// Image source for progressive display
CGImageSourceRef _imageSource;
/// Width of the downloaded image
int _imageWidth;
/// Height of the downloaded image
int _imageHeight;
/// Expected image size
long long _expectedSize;
/// Image orientation
UIImageOrientation _imageOrientation;
/// Connection queue
dispatch_queue_t _queue;
/// Url
NSURL* _url;
/// Delegate flags, avoid to many respondsToSelector
NyxDelegateFlags _delegateFlags;
}
@synthesize delegate = _delegate;
@synthesize caching = _caching;
@synthesize cacheTime = _cacheTime;
@synthesize downloading = _downloading;
#pragma mark - Allocations / Deallocations
-(id)init
{
if ((self = [super init]))
{
[self initializeAttributes];
}
return self;
}
-(id)initWithCoder:(NSCoder*)aDecoder
{
if ((self = [super initWithCoder:aDecoder]))
{
[self initializeAttributes];
}
return self;
}
-(id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
[self initializeAttributes];
}
return self;
}
-(id)initWithImage:(UIImage*)image
{
if ((self = [super initWithImage:image]))
{
[self initializeAttributes];
}
return self;
}
-(id)initWithImage:(UIImage*)image highlightedImage:(UIImage*)highlightedImage
{
if ((self = [super initWithImage:image highlightedImage:highlightedImage]))
{
[self initializeAttributes];
}
return self;
}
-(void)dealloc
{
NYX_DISPATCH_RELEASE(_queue);
_queue = NULL;
}
#pragma mark - Public
-(void)setDelegate:(id<NYXProgressiveImageViewDelegate>)delegate
{
if (delegate != _delegate)
{
_delegate = delegate;
_delegateFlags.delegateImageDidLoadWithImage = (unsigned)[delegate respondsToSelector:@selector(imageDidLoadWithImage:)];
_delegateFlags.delegateImageDownloadCompletedWithImage = (unsigned)[delegate respondsToSelector:@selector(imageDownloadCompletedWithImage:)];
_delegateFlags.delegateImageDownloadFailedWithData = (unsigned)[delegate respondsToSelector:@selector(imageDownloadFailedWithData:)];
}
}
-(void)loadImageAtURL:(NSURL*)url
{
if (_downloading)
return;
_url = url;
if (_caching)
{
NSFileManager* fileManager = [[NSFileManager alloc] init];
// check if file exists on cache
NSString* cacheDir = [NYXProgressiveImageView cacheDirectoryAddress];
NSString* cachedImagePath = [cacheDir stringByAppendingPathComponent:[self cachedImageSystemName]];
if ([fileManager fileExistsAtPath:cachedImagePath])
{
NSDate* mofificationDate = [[fileManager attributesOfItemAtPath:cachedImagePath error:nil] objectForKey:NSFileModificationDate];
// check modification date
if (-[mofificationDate timeIntervalSinceNow] > _cacheTime)
{
// Removes old cache file...
[self resetCache];
}
else
{
// Loads image from cache without networking
UIImage* localImage = [[UIImage alloc] initWithContentsOfFile:cachedImagePath];
dispatch_async(dispatch_get_main_queue(), ^{
self.image = localImage;
if (_delegateFlags.delegateImageDidLoadWithImage)
[_delegate imageDidLoadWithImage:localImage];
});
return;
}
}
}
dispatch_async(_queue, ^{
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:kNyxDefaultTimeoutValue];
_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
_downloading = YES;
[_connection start];
CFRunLoopRun();
});
}
+(void)resetImageCache
{
[[NSFileManager defaultManager] removeItemAtPath:[NYXProgressiveImageView cacheDirectoryAddress] error:nil];
}
#pragma mark - NSURLConnectionDelegate
-(void)connection:(__unused NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
_imageSource = CGImageSourceCreateIncremental(NULL);
_imageWidth = _imageHeight = -1;
_expectedSize = [response expectedContentLength];
_dataTemp = [[NSMutableData alloc] init];
}
-(void)connection:(__unused NSURLConnection*)connection didReceiveData:(NSData*)data
{
[_dataTemp appendData:data];
const long long len = (long long)[_dataTemp length];
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)_dataTemp, (len == _expectedSize) ? true : false);
if (_imageHeight > 0 && _imageWidth > 0)
{
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (cgImage)
{
//if (NYX_IOS_VERSION_LESS_THAN(@"5.0"))
//{
/// iOS 4.x fix to correctly handle JPEG images ( http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ )
/// If the image doesn't have a transparency layer, the background is black-filled
/// So we still need to render the image, it's teh sux.
/// Note: Progressive JPEG are not supported see #32
CGImageRef imgTmp = [self createTransitoryImage:cgImage];
if (imgTmp)
{
__block UIImage* img = [[UIImage alloc] initWithCGImage:imgTmp scale:1.0f orientation:_imageOrientation];
CGImageRelease(imgTmp);
dispatch_async(dispatch_get_main_queue(), ^{
self.image = img;
});
}
//}
//else
//{
//__block UIImage* img = [[UIImage alloc] initWithCGImage:cgImage];
//dispatch_async(dispatch_get_main_queue(), ^{
//self.image = img;
//});
//}
CGImageRelease(cgImage);
}
}
else
{
CFDictionaryRef dic = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (dic)
{
CFTypeRef val = CFDictionaryGetValue(dic, kCGImagePropertyPixelHeight);
if (val)
CFNumberGetValue(val, kCFNumberIntType, &_imageHeight);
val = CFDictionaryGetValue(dic, kCGImagePropertyPixelWidth);
if (val)
CFNumberGetValue(val, kCFNumberIntType, &_imageWidth);
val = CFDictionaryGetValue(dic, kCGImagePropertyOrientation);
if (val)
{
int orientation; // Note: This is an EXIF int for orientation, a number between 1 and 8
CFNumberGetValue(val, kCFNumberIntType, &orientation);
_imageOrientation = [NYXProgressiveImageView exifOrientationToiOSOrientation:orientation];
}
else
_imageOrientation = UIImageOrientationUp;
CFRelease(dic);
}
}
}
-(void)connectionDidFinishLoading:(__unused NSURLConnection*)connection
{
if (_dataTemp)
{
UIImage* img = [[UIImage alloc] initWithData:_dataTemp];
dispatch_sync(dispatch_get_main_queue(), ^{
if (_delegateFlags.delegateImageDownloadCompletedWithImage)
[_delegate imageDownloadCompletedWithImage:img];
self.image = img;
if (_delegateFlags.delegateImageDidLoadWithImage)
[_delegate imageDidLoadWithImage:img];
});
if (_caching)
{
// Create cache directory if it doesn't exist
BOOL isDir = YES;
NSFileManager* fileManager = [[NSFileManager alloc] init];
NSString* cacheDir = [NYXProgressiveImageView cacheDirectoryAddress];
if (![fileManager fileExistsAtPath:cacheDir isDirectory:&isDir])
[fileManager createDirectoryAtPath:cacheDir withIntermediateDirectories:NO attributes:nil error:nil];
NSString* path = [cacheDir stringByAppendingPathComponent:[self cachedImageSystemName]];
//[img saveToPath:path uti:CGImageSourceGetType(_imageSource) backgroundFillColor:nil];
[_dataTemp writeToFile:path options:NSDataWritingAtomic error:nil];
}
_dataTemp = nil;
}
if (_imageSource)
{
CFRelease(_imageSource);
_imageSource = NULL;
}
_connection = nil;
_url = nil;
_downloading = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}
-(void)connection:(__unused NSURLConnection*)connection didFailWithError:(__unused NSError*)error
{
if (_delegateFlags.delegateImageDownloadFailedWithData)
{
dispatch_sync(dispatch_get_main_queue(), ^{
[_delegate imageDownloadFailedWithData:_dataTemp];
});
}
_dataTemp = nil;
if (_imageSource)
{
CFRelease(_imageSource);
_imageSource = NULL;
}
_connection = nil;
_url = nil;
_downloading = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}
#pragma mark - Private
-(void)initializeAttributes
{
_cacheTime = kNyxDefaultCacheTimeValue;
_caching = NO;
_queue = dispatch_queue_create("com.cits.pdlqueue", DISPATCH_QUEUE_SERIAL);
_imageOrientation = UIImageOrientationUp;
_imageSource = NULL;
_dataTemp = nil;
}
-(CGImageRef)createTransitoryImage:(CGImageRef)partialImage
{
const size_t partialHeight = CGImageGetHeight(partialImage);
CGContextRef bmContext = NYXCreateARGBBitmapContext((size_t)_imageWidth, (size_t)_imageHeight, (size_t)_imageWidth * 4, NYXImageHasAlpha(partialImage));
if (!bmContext)
return NULL;
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _imageWidth, .size.height = partialHeight}, partialImage);
CGImageRef goodImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
return goodImageRef;
}
+(NSString*)cacheDirectoryAddress
{
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString* documentsDirectoryPath = [paths objectAtIndex:0];
return [documentsDirectoryPath stringByAppendingPathComponent:@"NYXProgressiveImageViewCache"];
}
-(NSString*)cachedImageSystemName
{
const char* concat_str = [[_url absoluteString] UTF8String];
if (!concat_str)
return @"";
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(concat_str, (CC_LONG)strlen(concat_str), result);
NSMutableString* hash = [[NSMutableString alloc] init];
for (unsigned int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
[hash appendFormat:@"%02X", result[i]];
return [hash lowercaseString];
}
-(void)resetCache
{
[[[NSFileManager alloc] init] removeItemAtPath:[[NYXProgressiveImageView cacheDirectoryAddress] stringByAppendingPathComponent:[self cachedImageSystemName]] error:nil];
}
+(UIImageOrientation)exifOrientationToiOSOrientation:(int)exifOrientation
{
UIImageOrientation orientation = UIImageOrientationUp;
switch (exifOrientation)
{
case 1:
orientation = UIImageOrientationUp;
break;
case 3:
orientation = UIImageOrientationDown;
break;
case 8:
orientation = UIImageOrientationLeft;
break;
case 6:
orientation = UIImageOrientationRight;
break;
case 2:
orientation = UIImageOrientationUpMirrored;
break;
case 4:
orientation = UIImageOrientationDownMirrored;
break;
case 5:
orientation = UIImageOrientationLeftMirrored;
break;
case 7:
orientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return orientation;
}
@end