Beta of PINAnimatedImage

This commit is contained in:
Garrett Moon
2016-04-05 18:02:09 -07:00
parent 54062d96d1
commit 5e2d20d36b
4 changed files with 1040 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
//
// PINAnimatedImage.h
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import <Foundation/Foundation.h>
#define PINAnimatedImageDebug 0
extern NSString *kPINAnimatedImageErrorDomain;
typedef NS_ENUM(NSUInteger, PINAnimatedImageError) {
PINAnimatedImageErrorNoError = 0,
PINAnimatedImageErrorFileCreationError,
PINAnimatedImageErrorFileHandleError,
PINAnimatedImageErrorImageFrameError,
PINAnimatedImageErrorMappingError,
};
typedef NS_ENUM(NSUInteger, PINAnimatedImageStatus) {
PINAnimatedImageStatusUnprocessed = 0,
PINAnimatedImageStatusInfoProcessed,
PINAnimatedImageStatusFirstFileProcessed,
PINAnimatedImageStatusProcessed,
PINAnimatedImageStatusCanceled,
PINAnimatedImageStatusError,
};
extern const size_t kPINAnimatedImageComponentsPerPixel;
extern const Float32 kPINAnimatedImageDefaultDuration;
//http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser
extern const Float32 kPINAnimatedImageMinimumDuration;
extern const NSTimeInterval kPINAnimatedImageDisplayRefreshRate;
typedef void(^PINAnimatedImageInfoReady)(UIImage *coverImage);
@interface PINAnimatedImage : NSObject
- (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData NS_DESIGNATED_INITIALIZER;
@property (nonatomic, strong, readwrite) PINAnimatedImageInfoReady infoCompletion;
@property (nonatomic, strong, readwrite) dispatch_block_t fileReady;
@property (nonatomic, strong, readwrite) dispatch_block_t animatedImageReady;
@property (nonatomic, assign, readwrite) PINAnimatedImageStatus status;
//Access to any properties or methods below this line before status == PINAnimatedImageStatusInfoProcessed is undefined.
@property (nonatomic, readonly) UIImage *coverImage;
@property (nonatomic, readonly) BOOL coverImageReady;
@property (nonatomic, readonly) BOOL playbackReady;
@property (nonatomic, readonly) CFTimeInterval totalDuration;
@property (nonatomic, readonly) NSUInteger frameInterval;
@property (nonatomic, readonly) size_t loopCount;
@property (nonatomic, readonly) size_t frameCount;
- (CGImageRef)imageAtIndex:(NSUInteger)index;
- (CFTimeInterval)durationAtIndex:(NSUInteger)index;
- (void)clearAnimatedImageCache;
@end

View File

@@ -0,0 +1,481 @@
//
// PINAnimatedImage.m
// Pods
//
// Created by Garrett Moon on 3/18/16.
//
//
#import "PINAnimatedImage.h"
#import "PINRemoteLock.m"
#import "PINAnimatedImageManager.h"
#import "PINRemoteImageMacros.h"
NSString *kPINAnimatedImageErrorDomain = @"kPINAnimatedImageErrorDomain";
const Float32 kPINAnimatedImageDefaultDuration = 0.1;
static const size_t kPINAnimatedImageBitsPerComponent = 8;
const size_t kPINAnimatedImageComponentsPerPixel = 4;
const NSTimeInterval kPINAnimatedImageDisplayRefreshRate = 60.0;
const Float32 kPINAnimatedImageMinimumDuration = 1 / kPINAnimatedImageDisplayRefreshRate;
@class PINSharedAnimatedImage;
@interface PINAnimatedImage ()
{
PINRemoteLock *_statusLock;
PINRemoteLock *_completionLock;
PINRemoteLock *_dataLock;
NSData *_currentData;
NSData *_nextData;
}
@property (nonatomic, strong, readonly) PINSharedAnimatedImage *sharedAnimatedImage;
@end
@implementation PINAnimatedImage
- (instancetype)init
{
return [self initWithAnimatedImageData:nil];
}
- (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData
{
if (self = [super init]) {
_statusLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage status lock"];
_completionLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage completion lock"];
_dataLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage data lock"];
NSAssert(animatedImageData != nil, @"animatedImageData must not be nil.");
_status = PINAnimatedImageStatusUnprocessed;
[[PINAnimatedImageManager sharedManager] animatedPathForImageData:animatedImageData infoCompletion:^(PINImage *coverImage, PINSharedAnimatedImage *shared) {
[_statusLock lockWithBlock:^{
_sharedAnimatedImage = shared;
if (_status == PINAnimatedImageStatusUnprocessed) {
_status = PINAnimatedImageStatusInfoProcessed;
}
}];
[_completionLock lockWithBlock:^{
if (_infoCompletion) {
_infoCompletion(coverImage);
}
}];
} completion:^(BOOL completed, NSString *path, NSError *error) {
__block BOOL success = NO;
[_statusLock lockWithBlock:^{
if (_status == PINAnimatedImageStatusInfoProcessed) {
_status = PINAnimatedImageStatusFirstFileProcessed;
}
if (completed && error == nil) {
_status = PINAnimatedImageStatusProcessed;
success = YES;
} else if (error) {
_status = PINAnimatedImageStatusError;
#if PINAnimatedImageDebug
NSLog(@"animated image error: %@", error);
#endif
}
}];
[_completionLock lockWithBlock:^{
if (_fileReady) {
_fileReady();
}
}];
if (success) {
[_completionLock lockWithBlock:^{
if (_animatedImageReady) {
_animatedImageReady();
}
}];
}
}];
}
return self;
}
- (void)setInfoCompletion:(PINAnimatedImageInfoReady)infoCompletion
{
[_completionLock lockWithBlock:^{
_infoCompletion = infoCompletion;
}];
}
- (void)setAnimatedImageReady:(dispatch_block_t)animatedImageReady
{
[_completionLock lockWithBlock:^{
_animatedImageReady = animatedImageReady;
}];
}
- (void)setFileReady:(dispatch_block_t)fileReady
{
[_completionLock lockWithBlock:^{
_fileReady = fileReady;
}];
}
- (PINImage *)coverImageWithMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
return [PINImage imageWithCGImage:[[self class] imageAtIndex:0 inMemoryMap:memoryMap width:width height:height bitmapInfo:bitmapInfo]];
}
void releaseData(void *data, const void *imageData, size_t size);
void releaseData(void *data, const void *imageData, size_t size)
{
CFRelease(data);
}
- (CGImageRef)imageAtIndex:(NSUInteger)index inSharedImageFiles:(NSArray <PINSharedAnimatedImageFile *>*)imageFiles width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
for (NSUInteger fileIdx = 0; fileIdx < imageFiles.count; fileIdx++) {
PINSharedAnimatedImageFile *imageFile = imageFiles[fileIdx];
if (index < imageFile.frameCount) {
__block NSData *memoryMappedData = nil;
[_dataLock lockWithBlock:^{
memoryMappedData = imageFile.memoryMappedData;
_currentData = memoryMappedData;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_dataLock lockWithBlock:^{
_nextData = (fileIdx + 1 < imageFiles.count) ? imageFiles[fileIdx + 1].memoryMappedData : imageFiles[0].memoryMappedData;
}];
});
}];
return [[self class] imageAtIndex:index inMemoryMap:memoryMappedData width:width height:height bitmapInfo:bitmapInfo];
} else {
index -= imageFile.frameCount;
}
}
//image file not done yet :(
return nil;
}
- (CFTimeInterval)durationAtIndex:(NSUInteger)index
{
return self.durations[index];
}
+ (CGImageRef)imageAtIndex:(NSUInteger)index inMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
Float32 outDuration;
size_t imageLength = width * height * kPINAnimatedImageComponentsPerPixel;
//frame duration + previous images
NSUInteger offset = sizeof(UInt32) + (index * (imageLength + sizeof(outDuration)));
[memoryMap getBytes:&outDuration range:NSMakeRange(offset, sizeof(outDuration))];
BytePtr imageData = (BytePtr)[memoryMap bytes];
imageData += offset + sizeof(outDuration);
NSAssert(offset + sizeof(outDuration) + imageLength <= memoryMap.length, @"Requesting frame beyond data bounds");
//retain the memory map, it will be released when releaseData is called
CFRetain((CFDataRef)memoryMap);
CGDataProviderRef dataProvider = CGDataProviderCreateWithData((void *)memoryMap, imageData, width * height * kPINAnimatedImageComponentsPerPixel, releaseData);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGImageRef imageRef = CGImageCreate(width,
height,
kPINAnimatedImageBitsPerComponent,
kPINAnimatedImageBitsPerComponent * kPINAnimatedImageComponentsPerPixel,
kPINAnimatedImageComponentsPerPixel * width,
colorSpace,
bitmapInfo,
dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
CFAutorelease(imageRef);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(dataProvider);
return imageRef;
}
+ (UInt32)widthFromMemoryMap:(NSData *)memoryMap
{
UInt32 width;
[memoryMap getBytes:&width range:NSMakeRange(2, sizeof(width))];
return width;
}
+ (UInt32)heightFromMemoryMap:(NSData *)memoryMap
{
UInt32 height;
[memoryMap getBytes:&height range:NSMakeRange(6, sizeof(height))];
return height;
}
+ (UInt32)loopCountFromMemoryMap:(NSData *)memoryMap
{
UInt32 loopCount;
[memoryMap getBytes:&loopCount range:NSMakeRange(10, sizeof(loopCount))];
return loopCount;
}
+ (UInt32)frameCountFromMemoryMap:(NSData *)memoryMap
{
UInt32 frameCount;
[memoryMap getBytes:&frameCount range:NSMakeRange(14, sizeof(frameCount))];
return frameCount;
}
+ (Float32 *)createDurationsFromMemoryMap:(NSData *)memoryMap frameCount:(UInt32)frameCount frameSize:(NSUInteger)frameSize totalDuration:(CFTimeInterval *)totalDuration
{
*totalDuration = 0;
Float32 *durations = (Float32 *)malloc(sizeof(Float32) * frameCount);
[memoryMap getBytes:&durations range:NSMakeRange(18, sizeof(Float32) * frameCount)];
for (NSUInteger idx = 0; idx < frameCount; idx++) {
*totalDuration += durations[idx];
}
return durations;
}
- (Float32 *)durations
{
return self.sharedAnimatedImage.durations;
}
- (CFTimeInterval)totalDuration
{
return self.sharedAnimatedImage.totalDuration;
}
- (size_t)loopCount
{
return self.sharedAnimatedImage.loopCount;
}
- (size_t)frameCount
{
return self.sharedAnimatedImage.frameCount;
}
- (size_t)width
{
return self.sharedAnimatedImage.width;
}
- (size_t)height
{
return self.sharedAnimatedImage.height;
}
- (PINAnimatedImageStatus)status
{
__block PINAnimatedImageStatus status;
[_statusLock lockWithBlock:^{
status = _status;
}];
return status;
}
- (CGImageRef)imageAtIndex:(NSUInteger)index
{
return [self imageAtIndex:index
inSharedImageFiles:self.sharedAnimatedImage.maps
width:(UInt32)self.sharedAnimatedImage.width
height:(UInt32)self.sharedAnimatedImage.height
bitmapInfo:self.sharedAnimatedImage.bitmapInfo];
}
- (PINImage *)coverImage
{
return self.sharedAnimatedImage.coverImage;
}
- (BOOL)coverImageReady
{
return self.status == PINAnimatedImageStatusInfoProcessed || self.status == PINAnimatedImageStatusFirstFileProcessed || self.status == PINAnimatedImageStatusProcessed;
}
- (BOOL)playbackReady
{
return self.status == PINAnimatedImageStatusProcessed || self.status == PINAnimatedImageStatusFirstFileProcessed;
}
- (void)clearAnimatedImageCache
{
[_dataLock lockWithBlock:^{
_currentData = nil;
_nextData = nil;
}];
}
- (NSUInteger)frameInterval
{
return MAX(self.minimumFrameInterval * kPINAnimatedImageDisplayRefreshRate, 1);
}
//Credit to FLAnimatedImage (https://github.com/Flipboard/FLAnimatedImage) for display link interval calculations
- (NSTimeInterval)minimumFrameInterval
{
const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kPINAnimatedImageMinimumDuration;
// Scales the frame delays by `kGreatestCommonDivisorPrecision`
// then converts it to an UInteger for in order to calculate the GCD.
NSUInteger scaledGCD = lrint(self.durations[0] * kGreatestCommonDivisorPrecision);
for (NSUInteger durationIdx = 0; durationIdx < self.frameCount; durationIdx++) {
Float32 duration = self.durations[durationIdx];
scaledGCD = gcd(lrint(duration * kGreatestCommonDivisorPrecision), scaledGCD);
}
// Reverse to scale to get the value back into seconds.
return (scaledGCD / kGreatestCommonDivisorPrecision);
}
//Credit to FLAnimatedImage (https://github.com/Flipboard/FLAnimatedImage) for display link interval calculations
static NSUInteger gcd(NSUInteger a, NSUInteger b)
{
// http://en.wikipedia.org/wiki/Greatest_common_divisor
if (a < b) {
return gcd(b, a);
} else if (a == b) {
return b;
}
while (true) {
NSUInteger remainder = a % b;
if (remainder == 0) {
return b;
}
a = b;
b = remainder;
}
}
@end
@implementation PINSharedAnimatedImage
- (instancetype)init
{
if (self = [super init]) {
_coverImageLock = [[PINRemoteLock alloc] initWithName:@"PINSharedAnimatedImage cover image lock"];
_completions = @[];
_infoCompletions = @[];
_maps = @[];
}
return self;
}
- (void)setInfoProcessedWithCoverImage:(PINImage *)coverImage durations:(Float32 *)durations totalDuration:(CFTimeInterval)totalDuration loopCount:(size_t)loopCount frameCount:(size_t)frameCount width:(size_t)width height:(size_t)height bitmapInfo:(CGBitmapInfo)bitmapInfo
{
NSAssert(_status == PINAnimatedImageStatusUnprocessed, @"Status should be unprocessed.");
[_coverImageLock lockWithBlock:^{
_coverImage = coverImage;
}];
_durations = (Float32 *)malloc(sizeof(Float32) * frameCount);
memcpy(_durations, durations, sizeof(Float32) * frameCount);
_totalDuration = totalDuration;
_loopCount = loopCount;
_frameCount = frameCount;
_width = width;
_height = height;
_bitmapInfo = bitmapInfo;
_status = PINAnimatedImageStatusInfoProcessed;
}
- (void)dealloc
{
free(_durations);
}
- (PINImage *)coverImage
{
__block PINImage *coverImage = nil;
[_coverImageLock lockWithBlock:^{
if (_coverImage == nil) {
coverImage = [PINImage imageWithCGImage:[PINAnimatedImage imageAtIndex:0 inMemoryMap:self.maps[0].memoryMappedData width:(UInt32)self.width height:(UInt32)self.height bitmapInfo:self.bitmapInfo]];
_coverImage = coverImage;
} else {
coverImage = _coverImage;
}
}];
return coverImage;
}
@end
@implementation PINSharedAnimatedImageFile
@synthesize memoryMappedData = _memoryMappedData;
@synthesize frameCount = _frameCount;
- (instancetype)init
{
NSAssert(NO, @"Call initWithPath:");
return [self initWithPath:nil];
}
- (instancetype)initWithPath:(NSString *)path
{
if (self = [super init]) {
_lock = [[PINRemoteLock alloc] initWithName:@"PINSharedAnimatedImageFile lock"];
_path = path;
}
return self;
}
- (UInt32)frameCount
{
__block UInt32 frameCount;
[_lock lockWithBlock:^{
if (_frameCount == 0) {
NSData *memoryMappedData = _memoryMappedData;
if (memoryMappedData == nil) {
memoryMappedData = [self loadMemoryMappedData];
}
[memoryMappedData getBytes:&_frameCount range:NSMakeRange(0, sizeof(_frameCount))];
}
frameCount = _frameCount;
}];
return frameCount;
}
- (NSData *)memoryMappedData
{
__block NSData *memoryMappedData;
[_lock lockWithBlock:^{
memoryMappedData = _memoryMappedData;
if (memoryMappedData == nil) {
memoryMappedData = [self loadMemoryMappedData];
}
}];
return memoryMappedData;
}
//must be called within lock
- (NSData *)loadMemoryMappedData
{
NSError *error = nil;
//local variable shenanigans due to weak ivar _memoryMappedData
NSData *memoryMappedData = [NSData dataWithContentsOfFile:self.path options:NSDataReadingMappedAlways error:&error];
if (error) {
#if PINAnimatedImageDebug
NSLog(@"Could not memory map data: %@", error);
#endif
} else {
_memoryMappedData = memoryMappedData;
}
return memoryMappedData;
}
@end

View File

@@ -0,0 +1,67 @@
//
// PINAnimatedImageManager.h
// Pods
//
// Created by Garrett Moon on 4/5/16.
//
//
#import <Foundation/Foundation.h>
#import "PINAnimatedImage.h"
@class PINRemoteLock;
@class PINSharedAnimatedImage;
@class PINSharedAnimatedImageFile;
typedef void(^PINAnimatedImageSharedReady)(UIImage *coverImage, PINSharedAnimatedImage *shared);
typedef void(^PINAnimatedImageDecodedPath)(BOOL finished, NSString *path, NSError *error);
@interface PINAnimatedImageManager : NSObject
+ (instancetype)sharedManager;
- (void)animatedPathForImageData:(NSData *)animatedImageData infoCompletion:(PINAnimatedImageSharedReady)infoCompletion completion:(PINAnimatedImageDecodedPath)completion;
@end
@interface PINSharedAnimatedImage : NSObject
{
PINRemoteLock *_coverImageLock;
}
//This is intentionally atomic. PINAnimatedImageManager must be able to add entries
//and clients must be able to read them concurrently.
@property (atomic, strong, readwrite) NSArray <PINSharedAnimatedImageFile *> *maps;
@property (nonatomic, strong, readwrite) NSArray <PINAnimatedImageDecodedPath> *completions;
@property (nonatomic, strong, readwrite) NSArray <PINAnimatedImageSharedReady> *infoCompletions;
@property (nonatomic, weak, readwrite) UIImage *coverImage;
@property (nonatomic, strong, readwrite) NSError *error;
//TODO is status thread safe?
@property (nonatomic, assign, readwrite) PINAnimatedImageStatus status;
- (void)setInfoProcessedWithCoverImage:(UIImage *)coverImage durations:(Float32 *)durations totalDuration:(CFTimeInterval)totalDuration loopCount:(size_t)loopCount frameCount:(size_t)frameCount width:(size_t)width height:(size_t)height bitmapInfo:(CGBitmapInfo)bitmapInfo;
@property (nonatomic, readonly) Float32 *durations;
@property (nonatomic, readonly) CFTimeInterval totalDuration;
@property (nonatomic, readonly) size_t loopCount;
@property (nonatomic, readonly) size_t frameCount;
@property (nonatomic, readonly) size_t width;
@property (nonatomic, readonly) size_t height;
@property (nonatomic, readonly) CGBitmapInfo bitmapInfo;
@end
@interface PINSharedAnimatedImageFile : NSObject
{
PINRemoteLock *_lock;
}
@property (nonatomic, strong, readonly) NSString *path;
@property (nonatomic, assign, readonly) UInt32 frameCount;
@property (nonatomic, weak, readonly) NSData *memoryMappedData;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end

View File

@@ -0,0 +1,429 @@
//
// PINAnimatedImageManager.m
// Pods
//
// Created by Garrett Moon on 4/5/16.
//
//
#import "PINAnimatedImageManager.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import "PINRemoteLock.h"
#import "PINRemoteImageMacros.h"
static const NSUInteger maxFileSize = 50000000; //max file size in bytes
static const Float32 maxFileDuration = 1; //max duration of a file in seconds
typedef void(^PINAnimatedImageInfoProcessed)(PINImage *coverImage, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, UInt32 bitmapInfo);
BOOL PINStatusCoverImageCompleted(PINAnimatedImageStatus status);
BOOL PINStatusCoverImageCompleted(PINAnimatedImageStatus status) {
return status == PINAnimatedImageStatusInfoProcessed || status == PINAnimatedImageStatusFirstFileProcessed || status == PINAnimatedImageStatusProcessed;
}
@interface PINAnimatedImageManager ()
{
PINRemoteLock *_lock;
}
+ (instancetype)sharedManager;
@property (nonatomic, strong, readonly) NSString *temporaryDirectory;
@property (nonatomic, strong, readonly) NSMutableDictionary <NSData *, PINSharedAnimatedImage *> *animatedImages;
@property (nonatomic, strong, readonly) dispatch_queue_t serialProcessingQueue;
@end
@implementation PINAnimatedImageManager
+ (instancetype)sharedManager
{
static dispatch_once_t onceToken;
static PINAnimatedImageManager *sharedManager;
dispatch_once(&onceToken, ^{
sharedManager = [[PINAnimatedImageManager alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
if (self = [super init]) {
//On iOS temp directories are not shared between apps. This may not be safe on OS X or other systems
_temporaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"PINAnimatedImageCache"];
[self cleanupFiles];
_lock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImageManager lock"];
if ([[NSFileManager defaultManager] fileExistsAtPath:_temporaryDirectory] == NO) {
[[NSFileManager defaultManager] createDirectoryAtPath:_temporaryDirectory withIntermediateDirectories:YES attributes:nil error:nil];
}
_animatedImages = [[NSMutableDictionary alloc] init];
_serialProcessingQueue = dispatch_queue_create("Serial animated image processing queue.", DISPATCH_QUEUE_SERIAL);
__weak PINAnimatedImageManager *weakSelf = self;
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
[weakSelf cleanupFiles];
}];
}
return self;
}
- (void)cleanupFiles
{
[[NSFileManager defaultManager] removeItemAtPath:self.temporaryDirectory error:nil];
}
- (void)animatedPathForImageData:(NSData *)animatedImageData infoCompletion:(PINAnimatedImageSharedReady)infoCompletion completion:(PINAnimatedImageDecodedPath)completion
{
__block BOOL startProcessing = NO;
{
[_lock lockWithBlock:^{
PINSharedAnimatedImage *shared = self.animatedImages[animatedImageData];
if (shared == nil) {
shared = [[PINSharedAnimatedImage alloc] init];
self.animatedImages[animatedImageData] = shared;
startProcessing = YES;
}
if (shared.status == PINAnimatedImageStatusProcessed) {
if (completion) {
completion(YES, nil, nil);
}
} else if (shared.error) {
if (completion) {
completion(NO, nil, shared.error);
}
} else {
if (completion) {
shared.completions = [shared.completions arrayByAddingObject:completion];
}
}
if (PINStatusCoverImageCompleted(shared.status)) {
if (infoCompletion) {
infoCompletion(shared.coverImage, shared);
}
} else {
if (infoCompletion) {
shared.infoCompletions = [shared.infoCompletions arrayByAddingObject:infoCompletion];
}
}
}];
}
if (startProcessing) {
dispatch_async(self.serialProcessingQueue, ^{
[[self class] processAnimatedImage:animatedImageData temporaryDirectory:self.temporaryDirectory infoCompletion:^(PINImage *coverImage, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, UInt32 bitmapInfo) {
__block NSArray *infoCompletions = nil;
__block PINSharedAnimatedImage *shared = nil;
[_lock lockWithBlock:^{
shared = self.animatedImages[animatedImageData];
[shared setInfoProcessedWithCoverImage:coverImage durations:durations totalDuration:totalDuration loopCount:loopCount frameCount:frameCount width:width height:height bitmapInfo:bitmapInfo];
infoCompletions = shared.infoCompletions;
shared.infoCompletions = @[];
}];
for (PINAnimatedImageSharedReady infoCompletion in infoCompletions) {
infoCompletion(coverImage, shared);
}
} decodedPath:^(BOOL finished, NSString *path, NSError *error) {
__block NSArray *completions = nil;
{
[_lock lockWithBlock:^{
PINSharedAnimatedImage *shared = self.animatedImages[animatedImageData];
if (path && error == nil) {
shared.maps = [shared.maps arrayByAddingObject:[[PINSharedAnimatedImageFile alloc] initWithPath:path]];
}
shared.error = error;
completions = shared.completions;
if (finished || error) {
shared.completions = @[];
}
if (finished) {
shared.status = PINAnimatedImageStatusProcessed;
} else {
shared.status = PINAnimatedImageStatusFirstFileProcessed;
}
}];
}
for (PINAnimatedImageDecodedPath completion in completions) {
completion(finished, path, error);
}
}];
});
}
}
+ (void)processAnimatedImage:(NSData *)animatedImageData
temporaryDirectory:(NSString *)temporaryDirectory
infoCompletion:(PINAnimatedImageInfoProcessed)infoCompletion
decodedPath:(PINAnimatedImageDecodedPath)completion
{
NSUUID *UUID = [NSUUID UUID];
NSError *error = nil;
NSString *filePath = nil;
//TODO Must handle file handle errors! Documentation says it throws exceptions on any errors :(
NSFileHandle *fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:0];
UInt32 width;
UInt32 height;
UInt32 bitmapInfo;
NSUInteger fileCount = 0;
UInt32 frameCountForFile = 0;
#if PINAnimatedImageDebug
CFTimeInterval start = CACurrentMediaTime();
#endif
if (fileHandle && error == nil) {
dispatch_queue_t diskWriteQueue = dispatch_queue_create("PINAnimatedImage disk write queue", DISPATCH_QUEUE_SERIAL);
dispatch_group_t diskGroup = dispatch_group_create();
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)animatedImageData,
(CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : (__bridge NSString *)kUTTypeGIF,
(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (imageSource) {
UInt32 frameCount = (UInt32)CGImageSourceGetCount(imageSource);
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, nil);
UInt32 loopCount = (UInt32)[[[imageProperties objectForKey:(__bridge NSString *)kCGImagePropertyGIFDictionary]
objectForKey:(__bridge NSString *)kCGImagePropertyGIFLoopCount] unsignedLongValue];
Float32 fileDuration = 0;
NSUInteger fileSize = 0;
Float32 durations[frameCount];
CFTimeInterval totalDuration = 0;
PINImage *coverImage = nil;
//Gather header file info
for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
if (frameIdx == 0) {
CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (frameImage == nil) {
error = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorImageFrameError userInfo:nil];
break;
}
bitmapInfo = CGImageGetBitmapInfo(frameImage);
width = (UInt32)CGImageGetWidth(frameImage);
height = (UInt32)CGImageGetHeight(frameImage);
coverImage = [PINImage imageWithCGImage:frameImage];
CGImageRelease(frameImage);
}
Float32 duration = [[self class] frameDurationAtIndex:frameIdx source:imageSource];
durations[frameIdx] = duration;
totalDuration += duration;
}
if (error == nil) {
//Get size, write file header get coverImage
//blockDurations will be freed below after calling infoCompletion
Float32 *blockDurations = (Float32 *)malloc(sizeof(Float32) * frameCount);
memcpy(blockDurations, durations, sizeof(Float32) * frameCount);
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[self writeFileHeader:fileHandle width:width height:height loopCount:loopCount frameCount:frameCount bitmapInfo:bitmapInfo durations:blockDurations];
[fileHandle closeFile];
});
fileCount = 1;
fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
dispatch_group_async(diskGroup, diskWriteQueue, ^{
PINLog(@"notifying info");
infoCompletion(coverImage, blockDurations, totalDuration, loopCount, frameCount, width, height, bitmapInfo);
free(blockDurations);
//write empty frame count
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
});
//Process frames
for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
@autoreleasepool {
if (fileDuration > maxFileDuration || fileSize > maxFileSize) {
//create a new file
dispatch_group_async(diskGroup, diskWriteQueue, ^{
//prepend file with frameCount
[fileHandle seekToFileOffset:0];
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
[fileHandle closeFile];
});
dispatch_group_async(diskGroup, diskWriteQueue, ^{
PINLog(@"notifying file: %@", filePath);
completion(NO, filePath, error);
});
diskGroup = dispatch_group_create();
fileCount++;
fileHandle = [self fileHandle:&error filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
frameCountForFile = 0;
fileDuration = 0;
fileSize = 0;
//write empty frame count
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
});
}
CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
if (frameImage == nil) {
error = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorImageFrameError userInfo:nil];
break;
}
Float32 duration = durations[frameIdx];
fileDuration += duration;
NSData *frameData = (__bridge_transfer NSData *)CGDataProviderCopyData(CGImageGetDataProvider(frameImage));
NSAssert(frameData.length == width * height * kPINAnimatedImageComponentsPerPixel, @"data should be width * height * 4 bytes");
dispatch_group_async(diskGroup, diskWriteQueue, ^{
[self writeFrameToFile:fileHandle duration:duration frameData:frameData];
});
CGImageRelease(frameImage);
frameCountForFile++;
}
}
}
CFRelease(imageSource);
}
dispatch_group_wait(diskGroup, DISPATCH_TIME_FOREVER);
//close the file handle
PINLog(@"closing last file: %@", fileHandle);
[fileHandle seekToFileOffset:0];
[fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
[fileHandle closeFile];
}
#if PINAnimatedImageDebug
CFTimeInterval interval = CACurrentMediaTime() - start;
NSLog(@"Encoding and write time: %f", interval);
#endif
completion(YES, filePath, error);
}
//http://stackoverflow.com/questions/16964366/delaytime-or-unclampeddelaytime-for-gifs
+ (Float32)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source
{
Float32 frameDuration = kPINAnimatedImageDefaultDuration;
NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, nil);
// use unclamped delay time before delay time before default
NSNumber *unclamedDelayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (unclamedDelayTime) {
frameDuration = [unclamedDelayTime floatValue];
} else {
NSNumber *delayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFDelayTime];
if (delayTime) {
frameDuration = [delayTime floatValue];
}
}
if (frameDuration < kPINAnimatedImageMinimumDuration) {
frameDuration = kPINAnimatedImageDefaultDuration;
}
return frameDuration;
}
+ (NSString *)filePathWithTemporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count
{
NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:[UUID UUIDString]];
if (count > 0) {
filePath = [filePath stringByAppendingString:[@(count) stringValue]];
}
return filePath;
}
+ (NSFileHandle *)fileHandle:(NSError **)error filePath:(NSString **)filePath temporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count;
{
NSString *outFilePath = [self filePathWithTemporaryDirectory:temporaryDirectory UUID:UUID count:count];
NSError *outError = nil;
NSFileHandle *fileHandle = nil;
if (outError == nil) {
BOOL success = [[NSFileManager defaultManager] createFileAtPath:outFilePath contents:nil attributes:nil];
if (success == NO) {
outError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileCreationError userInfo:nil];
}
}
if (outError == nil) {
fileHandle = [NSFileHandle fileHandleForWritingAtPath:outFilePath];
if (fileHandle == nil) {
outError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileHandleError userInfo:nil];
}
}
if (error) {
*error = outError;
}
if (filePath) {
*filePath = outFilePath;
}
return fileHandle;
}
/**
PINAnimatedImage file header
Header:
[version] 2 bytes
[width] 4 bytes
[height] 4 bytes
[loop count] 4 bytes
[frame count] 4 bytes
[bitmap info] 4 bytes
[durations] 4 bytes * frame count
*/
+ (void)writeFileHeader:(NSFileHandle *)fileHandle width:(UInt32)width height:(UInt32)height loopCount:(UInt32)loopCount frameCount:(UInt32)frameCount bitmapInfo:(UInt32)bitmapInfo durations:(Float32*)durations
{
UInt16 version = 1;
[fileHandle writeData:[NSData dataWithBytes:&version length:sizeof(version)]];
[fileHandle writeData:[NSData dataWithBytes:&width length:sizeof(width)]];
[fileHandle writeData:[NSData dataWithBytes:&height length:sizeof(height)]];
[fileHandle writeData:[NSData dataWithBytes:&loopCount length:sizeof(loopCount)]];
[fileHandle writeData:[NSData dataWithBytes:&frameCount length:sizeof(frameCount)]];
[fileHandle writeData:[NSData dataWithBytes:&bitmapInfo length:sizeof(bitmapInfo)]];
[fileHandle writeData:[NSData dataWithBytes:durations length:sizeof(Float32) * frameCount]];
}
/**
PINAnimatedImage frame file
[frame count(in file)] 4 bytes
[frame(s)]
Each frame:
[duration] 4 bytes
[frame data] width * height * 4 bytes
*/
+ (void)writeFrameToFile:(NSFileHandle *)fileHandle duration:(Float32)duration frameData:(NSData *)frameData
{
[fileHandle writeData:[NSData dataWithBytes:&duration length:sizeof(duration)]];
[fileHandle writeData:frameData];
}
@end