PINAnimatedImage.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. //
  2. // PINAnimatedImage.m
  3. // Pods
  4. //
  5. // Created by Garrett Moon on 3/18/16.
  6. //
  7. //
  8. #import "PINAnimatedImage.h"
  9. #import "PINRemoteLock.h"
  10. #import "PINAnimatedImageManager.h"
  11. NSString *kPINAnimatedImageErrorDomain = @"kPINAnimatedImageErrorDomain";
  12. const Float32 kPINAnimatedImageDefaultDuration = 0.1;
  13. static const size_t kPINAnimatedImageBitsPerComponent = 8;
  14. const NSTimeInterval kPINAnimatedImageDisplayRefreshRate = 60.0;
  15. //http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser
  16. const Float32 kPINAnimatedImageMinimumDuration = 1 / kPINAnimatedImageDisplayRefreshRate;
  17. @class PINSharedAnimatedImage;
  18. @interface PINAnimatedImage ()
  19. {
  20. PINRemoteLock *_completionLock;
  21. PINRemoteLock *_dataLock;
  22. NSData *_currentData;
  23. NSData *_nextData;
  24. }
  25. @property (atomic, strong, readwrite) PINSharedAnimatedImage *sharedAnimatedImage;
  26. @property (atomic, assign, readwrite) BOOL infoCompleted;
  27. @end
  28. @implementation PINAnimatedImage
  29. - (instancetype)init
  30. {
  31. return [self initWithAnimatedImageData:nil];
  32. }
  33. - (instancetype)initWithAnimatedImageData:(NSData *)animatedImageData
  34. {
  35. if (self = [super init]) {
  36. _completionLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage completion lock"];
  37. _dataLock = [[PINRemoteLock alloc] initWithName:@"PINAnimatedImage data lock"];
  38. NSAssert(animatedImageData != nil, @"animatedImageData must not be nil.");
  39. [[PINAnimatedImageManager sharedManager] animatedPathForImageData:animatedImageData infoCompletion:^(PINImage *coverImage, PINSharedAnimatedImage *shared) {
  40. self.sharedAnimatedImage = shared;
  41. self.infoCompleted = YES;
  42. [_completionLock lockWithBlock:^{
  43. if (_infoCompletion) {
  44. _infoCompletion(coverImage);
  45. _infoCompletion = nil;
  46. }
  47. }];
  48. } completion:^(BOOL completed, NSString *path, NSError *error) {
  49. BOOL success = NO;
  50. if (completed && error == nil) {
  51. success = YES;
  52. }
  53. [_completionLock lockWithBlock:^{
  54. if (_fileReady) {
  55. _fileReady();
  56. }
  57. }];
  58. if (success) {
  59. [_completionLock lockWithBlock:^{
  60. if (_animatedImageReady) {
  61. _animatedImageReady();
  62. _fileReady = nil;
  63. _animatedImageReady = nil;
  64. }
  65. }];
  66. }
  67. }];
  68. }
  69. return self;
  70. }
  71. - (void)setInfoCompletion:(PINAnimatedImageInfoReady)infoCompletion
  72. {
  73. [_completionLock lockWithBlock:^{
  74. _infoCompletion = infoCompletion;
  75. }];
  76. }
  77. - (void)setAnimatedImageReady:(dispatch_block_t)animatedImageReady
  78. {
  79. [_completionLock lockWithBlock:^{
  80. _animatedImageReady = animatedImageReady;
  81. }];
  82. }
  83. - (void)setFileReady:(dispatch_block_t)fileReady
  84. {
  85. [_completionLock lockWithBlock:^{
  86. _fileReady = fileReady;
  87. }];
  88. }
  89. - (PINImage *)coverImageWithMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitsPerPixel:(UInt32)bitsPerPixel bitmapInfo:(CGBitmapInfo)bitmapInfo
  90. {
  91. CGImageRef imageRef = [[self class] imageAtIndex:0 inMemoryMap:memoryMap width:width height:height bitsPerPixel:bitsPerPixel bitmapInfo:bitmapInfo];
  92. #if PIN_TARGET_IOS
  93. return [UIImage imageWithCGImage:imageRef];
  94. #elif PIN_TARGET_MAC
  95. return [[NSImage alloc] initWithCGImage:imageRef size:CGSizeMake(width, height)];
  96. #endif
  97. }
  98. void releaseData(void *data, const void *imageData, size_t size);
  99. void releaseData(void *data, const void *imageData, size_t size)
  100. {
  101. CFRelease(data);
  102. }
  103. - (CGImageRef)imageAtIndex:(NSUInteger)index inSharedImageFiles:(NSArray <PINSharedAnimatedImageFile *>*)imageFiles width:(UInt32)width height:(UInt32)height bitsPerPixel:(UInt32)bitsPerPixel bitmapInfo:(CGBitmapInfo)bitmapInfo
  104. {
  105. if (self.status == PINAnimatedImageStatusError) {
  106. return nil;
  107. }
  108. for (NSUInteger fileIdx = 0; fileIdx < imageFiles.count; fileIdx++) {
  109. PINSharedAnimatedImageFile *imageFile = imageFiles[fileIdx];
  110. if (index < imageFile.frameCount) {
  111. __block NSData *memoryMappedData = nil;
  112. [_dataLock lockWithBlock:^{
  113. memoryMappedData = imageFile.memoryMappedData;
  114. _currentData = memoryMappedData;
  115. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  116. [_dataLock lockWithBlock:^{
  117. _nextData = (fileIdx + 1 < imageFiles.count) ? imageFiles[fileIdx + 1].memoryMappedData : imageFiles[0].memoryMappedData;
  118. }];
  119. });
  120. }];
  121. return [[self class] imageAtIndex:index inMemoryMap:memoryMappedData width:width height:height bitsPerPixel:bitsPerPixel bitmapInfo:bitmapInfo];
  122. } else {
  123. index -= imageFile.frameCount;
  124. }
  125. }
  126. //image file not done yet :(
  127. return nil;
  128. }
  129. - (CFTimeInterval)durationAtIndex:(NSUInteger)index
  130. {
  131. return self.durations[index];
  132. }
  133. + (CGImageRef)imageAtIndex:(NSUInteger)index inMemoryMap:(NSData *)memoryMap width:(UInt32)width height:(UInt32)height bitsPerPixel:(UInt32)bitsPerPixel bitmapInfo:(CGBitmapInfo)bitmapInfo
  134. {
  135. if (memoryMap == nil) {
  136. return nil;
  137. }
  138. Float32 outDuration;
  139. const size_t imageLength = width * height * bitsPerPixel / 8;
  140. //frame duration + previous images
  141. NSUInteger offset = sizeof(UInt32) + (index * (imageLength + sizeof(outDuration)));
  142. [memoryMap getBytes:&outDuration range:NSMakeRange(offset, sizeof(outDuration))];
  143. BytePtr imageData = (BytePtr)[memoryMap bytes];
  144. imageData += offset + sizeof(outDuration);
  145. NSAssert(offset + sizeof(outDuration) + imageLength <= memoryMap.length, @"Requesting frame beyond data bounds");
  146. //retain the memory map, it will be released when releaseData is called
  147. CFRetain((CFDataRef)memoryMap);
  148. CGDataProviderRef dataProvider = CGDataProviderCreateWithData((void *)memoryMap, imageData, imageLength, releaseData);
  149. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  150. CGImageRef imageRef = CGImageCreate(width,
  151. height,
  152. kPINAnimatedImageBitsPerComponent,
  153. bitsPerPixel,
  154. bitsPerPixel / 8 * width,
  155. colorSpace,
  156. bitmapInfo,
  157. dataProvider,
  158. NULL,
  159. NO,
  160. kCGRenderingIntentDefault);
  161. if (imageRef) {
  162. CFAutorelease(imageRef);
  163. }
  164. CGColorSpaceRelease(colorSpace);
  165. CGDataProviderRelease(dataProvider);
  166. return imageRef;
  167. }
  168. // Although the following methods are unused, they could be in the future for restoring decoded images off disk (they're cleared currently).
  169. + (UInt32)widthFromMemoryMap:(NSData *)memoryMap
  170. {
  171. UInt32 width;
  172. [memoryMap getBytes:&width range:NSMakeRange(2, sizeof(width))];
  173. return width;
  174. }
  175. + (UInt32)heightFromMemoryMap:(NSData *)memoryMap
  176. {
  177. UInt32 height;
  178. [memoryMap getBytes:&height range:NSMakeRange(6, sizeof(height))];
  179. return height;
  180. }
  181. + (UInt32)bitsPerPixelFromMemoryMap:(NSData *)memoryMap
  182. {
  183. UInt32 bitsPerPixel;
  184. [memoryMap getBytes:&bitsPerPixel range:NSMakeRange(10, sizeof(bitsPerPixel))];
  185. return bitsPerPixel;
  186. }
  187. + (UInt32)loopCountFromMemoryMap:(NSData *)memoryMap
  188. {
  189. UInt32 loopCount;
  190. [memoryMap getBytes:&loopCount range:NSMakeRange(14, sizeof(loopCount))];
  191. return loopCount;
  192. }
  193. + (UInt32)frameCountFromMemoryMap:(NSData *)memoryMap
  194. {
  195. UInt32 frameCount;
  196. [memoryMap getBytes:&frameCount range:NSMakeRange(18, sizeof(frameCount))];
  197. return frameCount;
  198. }
  199. //durations should be a buffer of size Float32 * frameCount
  200. + (Float32 *)createDurations:(Float32 *)durations fromMemoryMap:(NSData *)memoryMap frameCount:(UInt32)frameCount frameSize:(NSUInteger)frameSize totalDuration:(nonnull CFTimeInterval *)totalDuration
  201. {
  202. *totalDuration = 0;
  203. [memoryMap getBytes:&durations range:NSMakeRange(22, sizeof(Float32) * frameCount)];
  204. for (NSUInteger idx = 0; idx < frameCount; idx++) {
  205. *totalDuration += durations[idx];
  206. }
  207. return durations;
  208. }
  209. - (Float32 *)durations
  210. {
  211. NSAssert([self infoReady], @"info must be ready");
  212. return self.sharedAnimatedImage.durations;
  213. }
  214. - (CFTimeInterval)totalDuration
  215. {
  216. NSAssert([self infoReady], @"info must be ready");
  217. return self.sharedAnimatedImage.totalDuration;
  218. }
  219. - (size_t)loopCount
  220. {
  221. NSAssert([self infoReady], @"info must be ready");
  222. return self.sharedAnimatedImage.loopCount;
  223. }
  224. - (size_t)frameCount
  225. {
  226. NSAssert([self infoReady], @"info must be ready");
  227. return self.sharedAnimatedImage.frameCount;
  228. }
  229. - (size_t)width
  230. {
  231. NSAssert([self infoReady], @"info must be ready");
  232. return self.sharedAnimatedImage.width;
  233. }
  234. - (size_t)height
  235. {
  236. NSAssert([self infoReady], @"info must be ready");
  237. return self.sharedAnimatedImage.height;
  238. }
  239. - (NSError *)error
  240. {
  241. return self.sharedAnimatedImage.error;
  242. }
  243. - (PINAnimatedImageStatus)status
  244. {
  245. if (self.sharedAnimatedImage == nil) {
  246. return PINAnimatedImageStatusUnprocessed;
  247. }
  248. return self.sharedAnimatedImage.status;
  249. }
  250. - (CGImageRef)imageAtIndex:(NSUInteger)index
  251. {
  252. return [self imageAtIndex:index
  253. inSharedImageFiles:self.sharedAnimatedImage.maps
  254. width:(UInt32)self.sharedAnimatedImage.width
  255. height:(UInt32)self.sharedAnimatedImage.height
  256. bitsPerPixel:(UInt32)self.sharedAnimatedImage.bitsPerPixel
  257. bitmapInfo:self.sharedAnimatedImage.bitmapInfo];
  258. }
  259. - (PINImage *)coverImage
  260. {
  261. NSAssert(self.coverImageReady, @"cover image must be ready.");
  262. return self.sharedAnimatedImage.coverImage;
  263. }
  264. - (BOOL)infoReady
  265. {
  266. return self.infoCompleted;
  267. }
  268. - (BOOL)coverImageReady
  269. {
  270. return self.status == PINAnimatedImageStatusInfoProcessed || self.status == PINAnimatedImageStatusFirstFileProcessed || self.status == PINAnimatedImageStatusProcessed;
  271. }
  272. - (BOOL)playbackReady
  273. {
  274. return self.status == PINAnimatedImageStatusProcessed || self.status == PINAnimatedImageStatusFirstFileProcessed;
  275. }
  276. - (void)clearAnimatedImageCache
  277. {
  278. [_dataLock lockWithBlock:^{
  279. _currentData = nil;
  280. _nextData = nil;
  281. }];
  282. }
  283. - (NSUInteger)frameInterval
  284. {
  285. return MAX(self.minimumFrameInterval * kPINAnimatedImageDisplayRefreshRate, 1);
  286. }
  287. //Credit to FLAnimatedImage ( https://github.com/Flipboard/FLAnimatedImage ) for display link interval calculations
  288. - (NSTimeInterval)minimumFrameInterval
  289. {
  290. const NSTimeInterval kGreatestCommonDivisorPrecision = 2.0 / kPINAnimatedImageMinimumDuration;
  291. // Scales the frame delays by `kGreatestCommonDivisorPrecision`
  292. // then converts it to an UInteger for in order to calculate the GCD.
  293. NSUInteger scaledGCD = lrint(self.durations[0] * kGreatestCommonDivisorPrecision);
  294. for (NSUInteger durationIdx = 0; durationIdx < self.frameCount; durationIdx++) {
  295. Float32 duration = self.durations[durationIdx];
  296. scaledGCD = gcd(lrint(duration * kGreatestCommonDivisorPrecision), scaledGCD);
  297. }
  298. // Reverse to scale to get the value back into seconds.
  299. return (scaledGCD / kGreatestCommonDivisorPrecision);
  300. }
  301. //Credit to FLAnimatedImage ( https://github.com/Flipboard/FLAnimatedImage ) for display link interval calculations
  302. static NSUInteger gcd(NSUInteger a, NSUInteger b)
  303. {
  304. // http://en.wikipedia.org/wiki/Greatest_common_divisor
  305. if (a < b) {
  306. return gcd(b, a);
  307. } else if (a == b) {
  308. return b;
  309. }
  310. while (true) {
  311. NSUInteger remainder = a % b;
  312. if (remainder == 0) {
  313. return b;
  314. }
  315. a = b;
  316. b = remainder;
  317. }
  318. }
  319. @end
  320. @implementation PINSharedAnimatedImage
  321. - (instancetype)init
  322. {
  323. if (self = [super init]) {
  324. _coverImageLock = [[PINRemoteLock alloc] initWithName:@"PINSharedAnimatedImage cover image lock"];
  325. _completions = @[];
  326. _infoCompletions = @[];
  327. _maps = @[];
  328. }
  329. return self;
  330. }
  331. - (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 bitsPerPixel:(size_t)bitsPerPixel bitmapInfo:(CGBitmapInfo)bitmapInfo
  332. {
  333. NSAssert(_status == PINAnimatedImageStatusUnprocessed, @"Status should be unprocessed.");
  334. [_coverImageLock lockWithBlock:^{
  335. _coverImage = coverImage;
  336. }];
  337. _UUID = UUID;
  338. _durations = (Float32 *)malloc(sizeof(Float32) * frameCount);
  339. memcpy(_durations, durations, sizeof(Float32) * frameCount);
  340. _totalDuration = totalDuration;
  341. _loopCount = loopCount;
  342. _frameCount = frameCount;
  343. _width = width;
  344. _height = height;
  345. _bitsPerPixel = bitsPerPixel;
  346. _bitmapInfo = bitmapInfo;
  347. _status = PINAnimatedImageStatusInfoProcessed;
  348. }
  349. - (void)dealloc
  350. {
  351. NSAssert(self.completions.count == 0 && self.infoCompletions.count == 0, @"Shouldn't be dealloc'd if we have a completion or an infoCompletion");
  352. //Clean up shared files.
  353. //Get references to maps and UUID so the below block doesn't reference self.
  354. NSArray *maps = self.maps;
  355. self.maps = nil;
  356. NSUUID *UUID = self.UUID;
  357. if (maps.count > 0) {
  358. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  359. //ignore errors
  360. [[NSFileManager defaultManager] removeItemAtPath:[PINAnimatedImageManager filePathWithTemporaryDirectory:[PINAnimatedImageManager temporaryDirectory] UUID:UUID count:0] error:nil];
  361. for (PINSharedAnimatedImageFile *file in maps) {
  362. [[NSFileManager defaultManager] removeItemAtPath:file.path error:nil];
  363. }
  364. });
  365. }
  366. free(_durations);
  367. }
  368. - (PINImage *)coverImage
  369. {
  370. __block PINImage *coverImage = nil;
  371. [_coverImageLock lockWithBlock:^{
  372. if (_coverImage == nil) {
  373. CGImageRef imageRef = [PINAnimatedImage imageAtIndex:0 inMemoryMap:self.maps[0].memoryMappedData width:(UInt32)self.width height:(UInt32)self.height bitsPerPixel:(UInt32)self.bitsPerPixel bitmapInfo:self.bitmapInfo];
  374. #if PIN_TARGET_IOS
  375. coverImage = [UIImage imageWithCGImage:imageRef];
  376. #elif PIN_TARGET_MAC
  377. coverImage = [[NSImage alloc] initWithCGImage:imageRef size:CGSizeMake(self.width, self.height)];
  378. #endif
  379. _coverImage = coverImage;
  380. } else {
  381. coverImage = _coverImage;
  382. }
  383. }];
  384. return coverImage;
  385. }
  386. @end
  387. @implementation PINSharedAnimatedImageFile
  388. @synthesize memoryMappedData = _memoryMappedData;
  389. @synthesize frameCount = _frameCount;
  390. - (instancetype)init
  391. {
  392. NSAssert(NO, @"Call initWithPath:");
  393. return [self initWithPath:nil];
  394. }
  395. - (instancetype)initWithPath:(NSString *)path
  396. {
  397. if (self = [super init]) {
  398. _lock = [[PINRemoteLock alloc] initWithName:@"PINSharedAnimatedImageFile lock"];
  399. _path = path;
  400. }
  401. return self;
  402. }
  403. - (UInt32)frameCount
  404. {
  405. __block UInt32 frameCount;
  406. [_lock lockWithBlock:^{
  407. if (_frameCount == 0) {
  408. NSData *memoryMappedData = _memoryMappedData;
  409. if (memoryMappedData == nil) {
  410. memoryMappedData = [self loadMemoryMappedData];
  411. }
  412. [memoryMappedData getBytes:&_frameCount range:NSMakeRange(0, sizeof(_frameCount))];
  413. }
  414. frameCount = _frameCount;
  415. }];
  416. return frameCount;
  417. }
  418. - (NSData *)memoryMappedData
  419. {
  420. __block NSData *memoryMappedData;
  421. [_lock lockWithBlock:^{
  422. memoryMappedData = _memoryMappedData;
  423. if (memoryMappedData == nil) {
  424. memoryMappedData = [self loadMemoryMappedData];
  425. }
  426. }];
  427. return memoryMappedData;
  428. }
  429. //must be called within lock
  430. - (NSData *)loadMemoryMappedData
  431. {
  432. NSError *error = nil;
  433. //local variable shenanigans due to weak ivar _memoryMappedData
  434. NSData *memoryMappedData = [NSData dataWithContentsOfFile:self.path options:NSDataReadingMappedAlways error:&error];
  435. if (error) {
  436. #if PINAnimatedImageDebug
  437. NSLog(@"Could not memory map data: %@", error);
  438. #endif
  439. } else {
  440. _memoryMappedData = memoryMappedData;
  441. }
  442. return memoryMappedData;
  443. }
  444. @end