mirror of
https://github.com/zhigang1992/PINRemoteImage.git
synced 2026-04-24 04:15:32 +08:00
524 lines
15 KiB
Objective-C
524 lines
15 KiB
Objective-C
//
|
|
// PINAnimatedImage.m
|
|
// Pods
|
|
//
|
|
// Created by Garrett Moon on 3/18/16.
|
|
//
|
|
//
|
|
|
|
#import "PINAnimatedImage.h"
|
|
|
|
#import "PINRemoteLock.h"
|
|
#import "PINAnimatedImageManager.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;
|
|
//http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser
|
|
const Float32 kPINAnimatedImageMinimumDuration = 1 / kPINAnimatedImageDisplayRefreshRate;
|
|
|
|
@class PINSharedAnimatedImage;
|
|
|
|
@interface PINAnimatedImage ()
|
|
{
|
|
PINRemoteLock *_completionLock;
|
|
PINRemoteLock *_dataLock;
|
|
|
|
NSData *_currentData;
|
|
NSData *_nextData;
|
|
}
|
|
|
|
@property (atomic, strong, readwrite) PINSharedAnimatedImage *sharedAnimatedImage;
|
|
@property (atomic, assign, readwrite) BOOL infoCompleted;
|
|
|
|
@end
|
|
|
|
@implementation PINAnimatedImage
|
|
|
|
- (instancetype)init
|
|
{
|
|
return [self initWithAnimatedImageData:nil];
|
|
}
|
|
|
|
- (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData
|
|
{
|
|
if (self = [super init]) {
|
|
_completionLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage completion lock"];
|
|
_dataLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage data lock"];
|
|
|
|
NSAssert(animatedImageData != nil, @"animatedImageData must not be nil.");
|
|
|
|
[[PINAnimatedImageManager sharedManager] animatedPathForImageData:animatedImageData infoCompletion:^(PINImage *coverImage, PINSharedAnimatedImage *shared) {
|
|
self.sharedAnimatedImage = shared;
|
|
self.infoCompleted = YES;
|
|
|
|
[_completionLock lockWithBlock:^{
|
|
if (_infoCompletion) {
|
|
_infoCompletion(coverImage);
|
|
_infoCompletion = nil;
|
|
}
|
|
}];
|
|
} completion:^(BOOL completed, NSString *path, NSError *error) {
|
|
BOOL success = NO;
|
|
|
|
if (completed && error == nil) {
|
|
success = YES;
|
|
}
|
|
|
|
[_completionLock lockWithBlock:^{
|
|
if (_fileReady) {
|
|
_fileReady();
|
|
}
|
|
}];
|
|
|
|
if (success) {
|
|
[_completionLock lockWithBlock:^{
|
|
if (_animatedImageReady) {
|
|
_animatedImageReady();
|
|
_fileReady = nil;
|
|
_animatedImageReady = nil;
|
|
}
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
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
|
|
{
|
|
CGImageRef imageRef = [[self class] imageAtIndex:0 inMemoryMap:memoryMap width:width height:height bitmapInfo:bitmapInfo];
|
|
#if PIN_TARGET_IOS
|
|
return [UIImage imageWithCGImage:imageRef];
|
|
#elif PIN_TARGET_MAC
|
|
return [[NSImage alloc] initWithCGImage:imageRef size:CGSizeMake(width, height)];
|
|
#endif
|
|
}
|
|
|
|
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
|
|
{
|
|
if (self.status == PINAnimatedImageStatusError) {
|
|
return nil;
|
|
}
|
|
|
|
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
|
|
{
|
|
if (memoryMap == nil) {
|
|
return nil;
|
|
}
|
|
|
|
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);
|
|
if (imageRef) {
|
|
CFAutorelease(imageRef);
|
|
}
|
|
|
|
CGColorSpaceRelease(colorSpace);
|
|
CGDataProviderRelease(dataProvider);
|
|
|
|
return imageRef;
|
|
}
|
|
|
|
// Although the following methods are unused, they could be in the future for restoring decoded images off disk (they're cleared currently).
|
|
+ (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;
|
|
}
|
|
|
|
//durations should be a buffer of size Float32 * frameCount
|
|
+ (Float32 *)createDurations:(Float32 *)durations fromMemoryMap:(NSData *)memoryMap frameCount:(UInt32)frameCount frameSize:(NSUInteger)frameSize totalDuration:(nonnull CFTimeInterval *)totalDuration
|
|
{
|
|
*totalDuration = 0;
|
|
[memoryMap getBytes:&durations range:NSMakeRange(18, sizeof(Float32) * frameCount)];
|
|
|
|
for (NSUInteger idx = 0; idx < frameCount; idx++) {
|
|
*totalDuration += durations[idx];
|
|
}
|
|
|
|
return durations;
|
|
}
|
|
|
|
- (Float32 *)durations
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.durations;
|
|
}
|
|
|
|
- (CFTimeInterval)totalDuration
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.totalDuration;
|
|
}
|
|
|
|
- (size_t)loopCount
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.loopCount;
|
|
}
|
|
|
|
- (size_t)frameCount
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.frameCount;
|
|
}
|
|
|
|
- (size_t)width
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.width;
|
|
}
|
|
|
|
- (size_t)height
|
|
{
|
|
NSAssert([self infoReady], @"info must be ready");
|
|
return self.sharedAnimatedImage.height;
|
|
}
|
|
|
|
- (NSError *)error
|
|
{
|
|
return self.sharedAnimatedImage.error;
|
|
}
|
|
|
|
- (PINAnimatedImageStatus)status
|
|
{
|
|
if (self.sharedAnimatedImage == nil) {
|
|
return PINAnimatedImageStatusUnprocessed;
|
|
}
|
|
return self.sharedAnimatedImage.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
|
|
{
|
|
NSAssert(self.coverImageReady, @"cover image must be ready.");
|
|
return self.sharedAnimatedImage.coverImage;
|
|
}
|
|
|
|
- (BOOL)infoReady
|
|
{
|
|
return self.infoCompleted;
|
|
}
|
|
|
|
- (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 UUID:(NSUUID *)UUID 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;
|
|
}];
|
|
_UUID = UUID;
|
|
_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
|
|
{
|
|
NSAssert(self.completions.count == 0 && self.infoCompletions.count == 0, @"Shouldn't be dealloc'd if we have a completion or an infoCompletion");
|
|
|
|
//Clean up shared files.
|
|
|
|
//Get references to maps and UUID so the below block doesn't reference self.
|
|
NSArray *maps = self.maps;
|
|
self.maps = nil;
|
|
NSUUID *UUID = self.UUID;
|
|
|
|
if (maps.count > 0) {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
//ignore errors
|
|
[[NSFileManager defaultManager] removeItemAtPath:[PINAnimatedImageManager filePathWithTemporaryDirectory:[PINAnimatedImageManager temporaryDirectory] UUID:UUID count:0] error:nil];
|
|
for (PINSharedAnimatedImageFile *file in maps) {
|
|
[[NSFileManager defaultManager] removeItemAtPath:file.path error:nil];
|
|
}
|
|
});
|
|
}
|
|
free(_durations);
|
|
}
|
|
|
|
- (PINImage *)coverImage
|
|
{
|
|
__block PINImage *coverImage = nil;
|
|
[_coverImageLock lockWithBlock:^{
|
|
if (_coverImage == nil) {
|
|
CGImageRef imageRef = [PINAnimatedImage imageAtIndex:0 inMemoryMap:self.maps[0].memoryMappedData width:(UInt32)self.width height:(UInt32)self.height bitmapInfo:self.bitmapInfo];
|
|
#if PIN_TARGET_IOS
|
|
coverImage = [UIImage imageWithCGImage:imageRef];
|
|
#elif PIN_TARGET_MAC
|
|
coverImage = [[NSImage alloc] initWithCGImage:imageRef size:CGSizeMake(self.width, self.height)];
|
|
#endif
|
|
_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
|