mirror of
https://github.com/zhigang1992/PINRemoteImage.git
synced 2026-04-29 04:35:15 +08:00
Beta of PINAnimatedImage
This commit is contained in:
63
Pod/Classes/PINAnimatedImage.h
Normal file
63
Pod/Classes/PINAnimatedImage.h
Normal 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
|
||||
481
Pod/Classes/PINAnimatedImage.m
Normal file
481
Pod/Classes/PINAnimatedImage.m
Normal 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
|
||||
67
Pod/Classes/PINAnimatedImageManager.h
Normal file
67
Pod/Classes/PINAnimatedImageManager.h
Normal 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
|
||||
429
Pod/Classes/PINAnimatedImageManager.m
Normal file
429
Pod/Classes/PINAnimatedImageManager.m
Normal 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
|
||||
Reference in New Issue
Block a user