PINAnimatedImageManager.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. //
  2. // PINAnimatedImageManager.m
  3. // Pods
  4. //
  5. // Created by Garrett Moon on 4/5/16.
  6. //
  7. //
  8. #import "PINAnimatedImageManager.h"
  9. #import <ImageIO/ImageIO.h>
  10. #if PIN_TARGET_IOS
  11. #import <MobileCoreServices/UTCoreTypes.h>
  12. #elif PIN_TARGET_MAC
  13. #import <CoreServices/CoreServices.h>
  14. #endif
  15. #import "PINRemoteLock.h"
  16. static const NSUInteger maxFileSize = 50000000; //max file size in bytes
  17. static const Float32 maxFileDuration = 1; //max duration of a file in seconds
  18. static const NSUInteger kCleanupAfterStartupDelay = 10; //clean up files after 10 seconds if it hasn't been done.
  19. typedef void(^PINAnimatedImageInfoProcessed)(PINImage *coverImage, NSUUID *UUID, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, size_t bitsPerPixel, UInt32 bitmapInfo);
  20. BOOL PINStatusCoverImageCompleted(PINAnimatedImageStatus status);
  21. BOOL PINStatusCoverImageCompleted(PINAnimatedImageStatus status) {
  22. return status == PINAnimatedImageStatusInfoProcessed || status == PINAnimatedImageStatusFirstFileProcessed || status == PINAnimatedImageStatusProcessed;
  23. }
  24. typedef NS_ENUM(NSUInteger, PINAnimatedImageManagerCondition) {
  25. PINAnimatedImageManagerConditionNotReady = 0,
  26. PINAnimatedImageManagerConditionReady = 1,
  27. };
  28. @interface PINAnimatedImageManager ()
  29. {
  30. NSConditionLock *_lock;
  31. }
  32. + (instancetype)sharedManager;
  33. @property (nonatomic, strong, readonly) NSMapTable <NSData *, PINSharedAnimatedImage *> *animatedImages;
  34. @property (nonatomic, strong, readonly) dispatch_queue_t serialProcessingQueue;
  35. @end
  36. @implementation PINAnimatedImageManager
  37. + (void)load
  38. {
  39. if (self == [PINAnimatedImageManager class]) {
  40. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kCleanupAfterStartupDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  41. //This forces a cleanup of files
  42. [PINAnimatedImageManager sharedManager];
  43. });
  44. }
  45. }
  46. + (instancetype)sharedManager
  47. {
  48. static dispatch_once_t onceToken;
  49. static PINAnimatedImageManager *sharedManager;
  50. dispatch_once(&onceToken, ^{
  51. sharedManager = [[PINAnimatedImageManager alloc] init];
  52. });
  53. return sharedManager;
  54. }
  55. + (NSString *)temporaryDirectory
  56. {
  57. static dispatch_once_t onceToken;
  58. static NSString *temporaryDirectory;
  59. dispatch_once(&onceToken, ^{
  60. //On iOS temp directories are not shared between apps. This may not be safe on OS X or other systems
  61. temporaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ASAnimatedImageCache"];
  62. });
  63. return temporaryDirectory;
  64. }
  65. - (instancetype)init
  66. {
  67. if (self = [super init]) {
  68. _lock = [[NSConditionLock alloc] initWithCondition:PINAnimatedImageManagerConditionNotReady];
  69. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  70. [_lock lockWhenCondition:PINAnimatedImageManagerConditionNotReady];
  71. [PINAnimatedImageManager cleanupFiles];
  72. if ([[NSFileManager defaultManager] fileExistsAtPath:[PINAnimatedImageManager temporaryDirectory]] == NO) {
  73. [[NSFileManager defaultManager] createDirectoryAtPath:[PINAnimatedImageManager temporaryDirectory] withIntermediateDirectories:YES attributes:nil error:nil];
  74. }
  75. [_lock unlockWithCondition:PINAnimatedImageManagerConditionReady];
  76. });
  77. _animatedImages = [[NSMapTable alloc] initWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableWeakMemory capacity:1];
  78. _serialProcessingQueue = dispatch_queue_create("Serial animated image processing queue.", DISPATCH_QUEUE_SERIAL);
  79. #if PIN_TARGET_IOS
  80. NSString * const notificationName = UIApplicationWillTerminateNotification;
  81. #elif PIN_TARGET_MAC
  82. NSString * const notificationName = NSApplicationWillTerminateNotification;
  83. #endif
  84. [[NSNotificationCenter defaultCenter] addObserverForName:notificationName
  85. object:nil
  86. queue:nil
  87. usingBlock:^(NSNotification * _Nonnull note) {
  88. [PINAnimatedImageManager cleanupFiles];
  89. }];
  90. }
  91. return self;
  92. }
  93. + (void)cleanupFiles
  94. {
  95. [[NSFileManager defaultManager] removeItemAtPath:[PINAnimatedImageManager temporaryDirectory] error:nil];
  96. }
  97. - (void)animatedPathForImageData:(NSData *)animatedImageData infoCompletion:(PINAnimatedImageSharedReady)infoCompletion completion:(PINAnimatedImageDecodedPath)completion
  98. {
  99. __block BOOL startProcessing = NO;
  100. __block PINSharedAnimatedImage *sharedAnimatedImage = nil;
  101. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  102. [_lock lockWhenCondition:PINAnimatedImageManagerConditionReady];
  103. sharedAnimatedImage = [self.animatedImages objectForKey:animatedImageData];
  104. if (sharedAnimatedImage == nil) {
  105. sharedAnimatedImage = [[PINSharedAnimatedImage alloc] init];
  106. [self.animatedImages setObject:sharedAnimatedImage forKey:animatedImageData];
  107. startProcessing = YES;
  108. }
  109. if (PINStatusCoverImageCompleted(sharedAnimatedImage.status)) {
  110. //Info is already processed, call infoCompletion immediately
  111. if (infoCompletion) {
  112. infoCompletion(sharedAnimatedImage.coverImage, sharedAnimatedImage);
  113. }
  114. } else {
  115. //Add infoCompletion to sharedAnimatedImage
  116. if (infoCompletion) {
  117. //Since ASSharedAnimatedImages are stored weakly in our map, we need a strong reference in completions
  118. PINAnimatedImageSharedReady capturingInfoCompletion = ^(PINImage *coverImage, PINSharedAnimatedImage *newShared) {
  119. __unused PINSharedAnimatedImage *strongShared = sharedAnimatedImage;
  120. infoCompletion(coverImage, newShared);
  121. };
  122. sharedAnimatedImage.infoCompletions = [sharedAnimatedImage.infoCompletions arrayByAddingObject:capturingInfoCompletion];
  123. }
  124. }
  125. if (sharedAnimatedImage.status == PINAnimatedImageStatusProcessed) {
  126. //Animated image is already fully processed, call completion immediately
  127. if (completion) {
  128. completion(YES, nil, nil);
  129. }
  130. } else if (sharedAnimatedImage.status == PINAnimatedImageStatusError) {
  131. if (completion) {
  132. completion(NO, nil, sharedAnimatedImage.error);
  133. }
  134. } else {
  135. //Add completion to sharedAnimatedImage
  136. if (completion) {
  137. //Since PINSharedAnimatedImages are stored weakly in our map, we need a strong reference in completions
  138. PINAnimatedImageDecodedPath capturingCompletion = ^(BOOL finished, NSString *path, NSError *error) {
  139. __unused PINSharedAnimatedImage *strongShared = sharedAnimatedImage;
  140. completion(finished, path, error);
  141. };
  142. sharedAnimatedImage.completions = [sharedAnimatedImage.completions arrayByAddingObject:capturingCompletion];
  143. }
  144. }
  145. [_lock unlockWithCondition:PINAnimatedImageManagerConditionReady];
  146. if (startProcessing) {
  147. dispatch_async(self.serialProcessingQueue, ^{
  148. [[self class] processAnimatedImage:animatedImageData temporaryDirectory:[PINAnimatedImageManager temporaryDirectory] infoCompletion:^(PINImage *coverImage, NSUUID *UUID, Float32 *durations, CFTimeInterval totalDuration, size_t loopCount, size_t frameCount, size_t width, size_t height, size_t bitsPerPixel, UInt32 bitmapInfo) {
  149. __block NSArray *infoCompletions = nil;
  150. __block PINSharedAnimatedImage *sharedAnimatedImage = nil;
  151. [_lock lockWhenCondition:PINAnimatedImageManagerConditionReady];
  152. sharedAnimatedImage = [self.animatedImages objectForKey:animatedImageData];
  153. [sharedAnimatedImage setInfoProcessedWithCoverImage:coverImage UUID:UUID durations:durations totalDuration:totalDuration loopCount:loopCount frameCount:frameCount width:width height:height bitsPerPixel:bitsPerPixel bitmapInfo:bitmapInfo];
  154. infoCompletions = sharedAnimatedImage.infoCompletions;
  155. sharedAnimatedImage.infoCompletions = @[];
  156. [_lock unlockWithCondition:PINAnimatedImageManagerConditionReady];
  157. for (PINAnimatedImageSharedReady infoCompletion in infoCompletions) {
  158. infoCompletion(coverImage, sharedAnimatedImage);
  159. }
  160. } decodedPath:^(BOOL finished, NSString *path, NSError *error) {
  161. __block NSArray *completions = nil;
  162. {
  163. [_lock lockWhenCondition:PINAnimatedImageManagerConditionReady];
  164. PINSharedAnimatedImage *sharedAnimatedImage = [self.animatedImages objectForKey:animatedImageData];
  165. if (path && error == nil) {
  166. sharedAnimatedImage.maps = [sharedAnimatedImage.maps arrayByAddingObject:[[PINSharedAnimatedImageFile alloc] initWithPath:path]];
  167. }
  168. sharedAnimatedImage.error = error;
  169. if (error) {
  170. sharedAnimatedImage.status = PINAnimatedImageStatusError;
  171. }
  172. completions = sharedAnimatedImage.completions;
  173. if (finished || error) {
  174. sharedAnimatedImage.completions = @[];
  175. }
  176. if (error == nil) {
  177. if (finished) {
  178. sharedAnimatedImage.status = PINAnimatedImageStatusProcessed;
  179. } else {
  180. sharedAnimatedImage.status = PINAnimatedImageStatusFirstFileProcessed;
  181. }
  182. }
  183. [_lock unlockWithCondition:PINAnimatedImageManagerConditionReady];
  184. }
  185. for (PINAnimatedImageDecodedPath completion in completions) {
  186. completion(finished, path, error);
  187. }
  188. }];
  189. });
  190. }
  191. });
  192. }
  193. #define HANDLE_PROCESSING_ERROR(ERROR) \
  194. { \
  195. if (ERROR != nil) { \
  196. [errorLock lockWithBlock:^{ \
  197. if (processingError == nil) { \
  198. processingError = ERROR; \
  199. } \
  200. }]; \
  201. \
  202. [fileHandle closeFile]; \
  203. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; \
  204. } \
  205. }
  206. #define PROCESSING_ERROR \
  207. ({__block NSError *ERROR; \
  208. [errorLock lockWithBlock:^{ \
  209. ERROR = processingError; \
  210. }]; \
  211. ERROR;}) \
  212. + (void)processAnimatedImage:(NSData *)animatedImageData
  213. temporaryDirectory:(NSString *)temporaryDirectory
  214. infoCompletion:(PINAnimatedImageInfoProcessed)infoCompletion
  215. decodedPath:(PINAnimatedImageDecodedPath)completion
  216. {
  217. NSUUID *UUID = [NSUUID UUID];
  218. __block NSError *processingError = nil;
  219. PINRemoteLock *errorLock = [[PINRemoteLock alloc] initWithName:@"animatedImage processing lock"];
  220. NSString *filePath = nil;
  221. //TODO Must handle file handle errors! Documentation says it throws exceptions on any errors :(
  222. NSError *fileHandleError = nil;
  223. NSFileHandle *fileHandle = [self fileHandle:&fileHandleError filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:0];
  224. HANDLE_PROCESSING_ERROR(fileHandleError);
  225. UInt32 width;
  226. UInt32 height;
  227. UInt32 bitsPerPixel;
  228. UInt32 bitmapInfo;
  229. NSUInteger fileCount = 0;
  230. UInt32 frameCountForFile = 0;
  231. Float32 *durations = NULL;
  232. #if PINAnimatedImageDebug
  233. CFTimeInterval start = CACurrentMediaTime();
  234. #endif
  235. if (fileHandle && PROCESSING_ERROR == nil) {
  236. dispatch_queue_t diskWriteQueue = dispatch_queue_create("PINAnimatedImage disk write queue", DISPATCH_QUEUE_SERIAL);
  237. dispatch_group_t diskGroup = dispatch_group_create();
  238. CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)animatedImageData,
  239. (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : (__bridge NSString *)kUTTypeGIF,
  240. (__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
  241. if (imageSource) {
  242. UInt32 frameCount = (UInt32)CGImageSourceGetCount(imageSource);
  243. NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, nil);
  244. UInt32 loopCount = (UInt32)[[[imageProperties objectForKey:(__bridge NSString *)kCGImagePropertyGIFDictionary]
  245. objectForKey:(__bridge NSString *)kCGImagePropertyGIFLoopCount] unsignedLongValue];
  246. Float32 fileDuration = 0;
  247. NSUInteger fileSize = 0;
  248. durations = (Float32 *)malloc(sizeof(Float32) * frameCount);
  249. CFTimeInterval totalDuration = 0;
  250. PINImage *coverImage = nil;
  251. //Gather header file info
  252. for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
  253. if (frameIdx == 0) {
  254. CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
  255. if (frameImage == nil) {
  256. NSError *frameError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorImageFrameError userInfo:nil];
  257. HANDLE_PROCESSING_ERROR(frameError);
  258. break;
  259. }
  260. bitmapInfo = CGImageGetBitmapInfo(frameImage);
  261. width = (UInt32)CGImageGetWidth(frameImage);
  262. height = (UInt32)CGImageGetHeight(frameImage);
  263. bitsPerPixel = (UInt32)CGImageGetBitsPerPixel(frameImage);
  264. #if PIN_TARGET_IOS
  265. coverImage = [UIImage imageWithCGImage:frameImage];
  266. #elif PIN_TARGET_MAC
  267. coverImage = [[NSImage alloc] initWithCGImage:frameImage size:CGSizeMake(width, height)];
  268. #endif
  269. CGImageRelease(frameImage);
  270. }
  271. Float32 duration = [[self class] frameDurationAtIndex:frameIdx source:imageSource];
  272. durations[frameIdx] = duration;
  273. totalDuration += duration;
  274. }
  275. if (PROCESSING_ERROR == nil) {
  276. //Get size, write file header get coverImage
  277. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  278. NSError *fileHeaderError = [self writeFileHeader:fileHandle width:width height:height bitsPerPixel:bitsPerPixel loopCount:loopCount frameCount:frameCount bitmapInfo:bitmapInfo durations:durations];
  279. HANDLE_PROCESSING_ERROR(fileHeaderError);
  280. if (fileHeaderError == nil) {
  281. [fileHandle closeFile];
  282. PINLog(@"notifying info");
  283. infoCompletion(coverImage, UUID, durations, totalDuration, loopCount, frameCount, width, height, bitsPerPixel, bitmapInfo);
  284. }
  285. });
  286. fileCount = 1;
  287. NSError *fileHandleError = nil;
  288. fileHandle = [self fileHandle:&fileHandleError filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
  289. HANDLE_PROCESSING_ERROR(fileHandleError);
  290. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  291. //write empty frame count
  292. @try {
  293. [fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
  294. } @catch (NSException *exception) {
  295. NSError *frameCountError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  296. HANDLE_PROCESSING_ERROR(frameCountError);
  297. } @finally {}
  298. });
  299. //Process frames
  300. for (NSUInteger frameIdx = 0; frameIdx < frameCount; frameIdx++) {
  301. if (PROCESSING_ERROR != nil) {
  302. break;
  303. }
  304. @autoreleasepool {
  305. if (fileDuration > maxFileDuration || fileSize > maxFileSize) {
  306. //create a new file
  307. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  308. //prepend file with frameCount
  309. @try {
  310. [fileHandle seekToFileOffset:0];
  311. [fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
  312. [fileHandle closeFile];
  313. } @catch (NSException *exception) {
  314. NSError *frameCountError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  315. HANDLE_PROCESSING_ERROR(frameCountError);
  316. } @finally {}
  317. });
  318. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  319. PINLog(@"notifying file: %@", filePath);
  320. completion(NO, filePath, PROCESSING_ERROR);
  321. });
  322. diskGroup = dispatch_group_create();
  323. fileCount++;
  324. NSError *fileHandleError = nil;
  325. fileHandle = [self fileHandle:&fileHandleError filePath:&filePath temporaryDirectory:temporaryDirectory UUID:UUID count:fileCount];
  326. HANDLE_PROCESSING_ERROR(fileHandleError);
  327. frameCountForFile = 0;
  328. fileDuration = 0;
  329. fileSize = 0;
  330. //write empty frame count
  331. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  332. @try {
  333. [fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
  334. } @catch (NSException *exception) {
  335. NSError *frameCountError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  336. HANDLE_PROCESSING_ERROR(frameCountError);
  337. } @finally {}
  338. });
  339. }
  340. Float32 duration = durations[frameIdx];
  341. fileDuration += duration;
  342. dispatch_group_async(diskGroup, diskWriteQueue, ^{
  343. if (PROCESSING_ERROR) {
  344. return;
  345. }
  346. CGImageRef frameImage = CGImageSourceCreateImageAtIndex(imageSource, frameIdx, (CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceShouldCache : (__bridge NSNumber *)kCFBooleanFalse});
  347. if (frameImage == nil) {
  348. NSError *frameImageError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorImageFrameError userInfo:nil];
  349. HANDLE_PROCESSING_ERROR(frameImageError);
  350. return;
  351. }
  352. NSData *frameData = (__bridge_transfer NSData *)CGDataProviderCopyData(CGImageGetDataProvider(frameImage));
  353. NSAssert(frameData.length == width * height * bitsPerPixel / 8, @"data should be width * height * bytes per pixel");
  354. NSError *frameWriteError = [self writeFrameToFile:fileHandle duration:duration frameData:frameData];
  355. HANDLE_PROCESSING_ERROR(frameWriteError);
  356. CGImageRelease(frameImage);
  357. });
  358. frameCountForFile++;
  359. }
  360. }
  361. } else {
  362. completion(NO, nil, PROCESSING_ERROR);
  363. }
  364. }
  365. dispatch_group_wait(diskGroup, DISPATCH_TIME_FOREVER);
  366. if (imageSource) {
  367. CFRelease(imageSource);
  368. }
  369. //close the file handle
  370. PINLog(@"closing last file: %@", fileHandle);
  371. @try {
  372. [fileHandle seekToFileOffset:0];
  373. [fileHandle writeData:[NSData dataWithBytes:&frameCountForFile length:sizeof(frameCountForFile)]];
  374. [fileHandle closeFile];
  375. } @catch (NSException *exception) {
  376. NSError *frameCountError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  377. HANDLE_PROCESSING_ERROR(frameCountError);
  378. } @finally {}
  379. }
  380. #if PINAnimatedImageDebug
  381. CFTimeInterval interval = CACurrentMediaTime() - start;
  382. NSLog(@"Encoding and write time: %f", interval);
  383. #endif
  384. if (durations) {
  385. free(durations);
  386. }
  387. completion(YES, filePath, PROCESSING_ERROR);
  388. }
  389. //http://stackoverflow.com/questions/16964366/delaytime-or-unclampeddelaytime-for-gifs
  390. + (Float32)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source
  391. {
  392. Float32 frameDuration = kPINAnimatedImageDefaultDuration;
  393. NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, nil);
  394. // use unclamped delay time before delay time before default
  395. NSNumber *unclamedDelayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFUnclampedDelayTime];
  396. if (unclamedDelayTime) {
  397. frameDuration = [unclamedDelayTime floatValue];
  398. } else {
  399. NSNumber *delayTime = frameProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary][(__bridge NSString *)kCGImagePropertyGIFDelayTime];
  400. if (delayTime) {
  401. frameDuration = [delayTime floatValue];
  402. }
  403. }
  404. if (frameDuration < kPINAnimatedImageMinimumDuration) {
  405. frameDuration = kPINAnimatedImageDefaultDuration;
  406. }
  407. return frameDuration;
  408. }
  409. + (NSString *)filePathWithTemporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count
  410. {
  411. NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:[UUID UUIDString]];
  412. if (count > 0) {
  413. filePath = [filePath stringByAppendingString:[@(count) stringValue]];
  414. }
  415. return filePath;
  416. }
  417. + (NSFileHandle *)fileHandle:(NSError **)error filePath:(NSString **)filePath temporaryDirectory:(NSString *)temporaryDirectory UUID:(NSUUID *)UUID count:(NSUInteger)count;
  418. {
  419. NSString *outFilePath = [self filePathWithTemporaryDirectory:temporaryDirectory UUID:UUID count:count];
  420. NSError *outError = nil;
  421. NSFileHandle *fileHandle = nil;
  422. if (outError == nil) {
  423. BOOL success = [[NSFileManager defaultManager] createFileAtPath:outFilePath contents:nil attributes:nil];
  424. if (success == NO) {
  425. outError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileCreationError userInfo:nil];
  426. }
  427. }
  428. if (outError == nil) {
  429. fileHandle = [NSFileHandle fileHandleForWritingAtPath:outFilePath];
  430. if (fileHandle == nil) {
  431. outError = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileHandleError userInfo:nil];
  432. }
  433. }
  434. if (error) {
  435. *error = outError;
  436. }
  437. if (filePath) {
  438. *filePath = outFilePath;
  439. }
  440. return fileHandle;
  441. }
  442. /**
  443. PINAnimatedImage file header
  444. Header:
  445. [version] 2 bytes
  446. [width] 4 bytes
  447. [height] 4 bytes
  448. [loop count] 4 bytes
  449. [frame count] 4 bytes
  450. [bitmap info] 4 bytes
  451. [durations] 4 bytes * frame count
  452. */
  453. + (NSError *)writeFileHeader:(NSFileHandle *)fileHandle width:(UInt32)width height:(UInt32)height bitsPerPixel:(UInt32)bitsPerPixel loopCount:(UInt32)loopCount frameCount:(UInt32)frameCount bitmapInfo:(UInt32)bitmapInfo durations:(Float32*)durations
  454. {
  455. NSError *error = nil;
  456. @try {
  457. UInt16 version = 2;
  458. [fileHandle writeData:[NSData dataWithBytes:&version length:sizeof(version)]];
  459. [fileHandle writeData:[NSData dataWithBytes:&width length:sizeof(width)]];
  460. [fileHandle writeData:[NSData dataWithBytes:&height length:sizeof(height)]];
  461. [fileHandle writeData:[NSData dataWithBytes:&bitsPerPixel length:sizeof(bitsPerPixel)]];
  462. [fileHandle writeData:[NSData dataWithBytes:&loopCount length:sizeof(loopCount)]];
  463. [fileHandle writeData:[NSData dataWithBytes:&frameCount length:sizeof(frameCount)]];
  464. [fileHandle writeData:[NSData dataWithBytes:&bitmapInfo length:sizeof(bitmapInfo)]];
  465. //Since we can't get the length of the durations array from the pointer, we'll just calculate it based on the frameCount.
  466. [fileHandle writeData:[NSData dataWithBytes:durations length:sizeof(Float32) * frameCount]];
  467. } @catch (NSException *exception) {
  468. error = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  469. } @finally {}
  470. return error;
  471. }
  472. /**
  473. PINAnimatedImage frame file
  474. [frame count(in file)] 4 bytes
  475. [frame(s)]
  476. Each frame:
  477. [duration] 4 bytes
  478. [frame data] width * height * 4 bytes
  479. */
  480. + (NSError *)writeFrameToFile:(NSFileHandle *)fileHandle duration:(Float32)duration frameData:(NSData *)frameData
  481. {
  482. NSError *error = nil;
  483. @try {
  484. [fileHandle writeData:[NSData dataWithBytes:&duration length:sizeof(duration)]];
  485. [fileHandle writeData:frameData];
  486. } @catch (NSException *exception) {
  487. error = [NSError errorWithDomain:kPINAnimatedImageErrorDomain code:PINAnimatedImageErrorFileWrite userInfo:@{@"NSException" : exception}];
  488. } @finally {}
  489. return error;
  490. }
  491. @end