PINAnimatedImageManager.m 23 KB

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