YBImage.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. //
  2. // YBImage.m
  3. // YBImageBrowserDemo
  4. //
  5. // Created by 波儿菜 on 2018/8/31.
  6. // Copyright © 2018年 波儿菜. All rights reserved.
  7. //
  8. #import "YBImage.h"
  9. #import "YBIBImageData.h"
  10. #import "YBIBUtilities.h"
  11. /**
  12. An array of NSNumber objects, shows the best order for path scale search.
  13. e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1] iPhone6 Plus:@[@3,@2,@1]
  14. */
  15. static NSArray *_NSBundlePreferredScales() {
  16. static NSArray *scales;
  17. static dispatch_once_t onceToken;
  18. dispatch_once(&onceToken, ^{
  19. CGFloat screenScale = [UIScreen mainScreen].scale;
  20. if (screenScale <= 1) {
  21. scales = @[@1,@2,@3];
  22. } else if (screenScale <= 2) {
  23. scales = @[@2,@3,@1];
  24. } else {
  25. scales = @[@3,@2,@1];
  26. }
  27. });
  28. return scales;
  29. }
  30. /**
  31. Add scale modifier to the file name (without path extension),
  32. From @"name" to @"name@2x".
  33. e.g.
  34. <table>
  35. <tr><th>Before </th><th>After(scale:2)</th></tr>
  36. <tr><td>"icon" </td><td>"icon@2x" </td></tr>
  37. <tr><td>"icon " </td><td>"icon @2x" </td></tr>
  38. <tr><td>"icon.top" </td><td>"icon.top@2x" </td></tr>
  39. <tr><td>"/p/name" </td><td>"/p/name@2x" </td></tr>
  40. <tr><td>"/path/" </td><td>"/path/" </td></tr>
  41. </table>
  42. @param scale Resource scale.
  43. @return String by add scale modifier, or just return if it's not end with file name.
  44. */
  45. static NSString *_NSStringByAppendingNameScale(NSString *string, CGFloat scale) {
  46. if (!string) return nil;
  47. if (fabs(scale - 1) <= __FLT_EPSILON__ || string.length == 0 || [string hasSuffix:@"/"]) return string.copy;
  48. return [string stringByAppendingFormat:@"@%@x", @(scale)];
  49. }
  50. /**
  51. Return the path scale.
  52. e.g.
  53. <table>
  54. <tr><th>Path </th><th>Scale </th></tr>
  55. <tr><td>"icon.png" </td><td>1 </td></tr>
  56. <tr><td>"icon@2x.png" </td><td>2 </td></tr>
  57. <tr><td>"icon@2.5x.png" </td><td>2.5 </td></tr>
  58. <tr><td>"icon@2x" </td><td>1 </td></tr>
  59. <tr><td>"icon@2x..png" </td><td>1 </td></tr>
  60. <tr><td>"icon@2x.png/" </td><td>1 </td></tr>
  61. </table>
  62. */
  63. static CGFloat _NSStringPathScale(NSString *string) {
  64. if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
  65. NSString *name = string.stringByDeletingPathExtension;
  66. __block CGFloat scale = 1;
  67. NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil];
  68. [pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
  69. if (result.range.location >= 3) {
  70. scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue;
  71. }
  72. }];
  73. return scale;
  74. }
  75. @implementation YBImage {
  76. YYImageDecoder *_decoder;
  77. NSArray *_preloadedFrames;
  78. dispatch_semaphore_t _preloadedLock;
  79. NSUInteger _bytesPerFrame;
  80. }
  81. + (__kindof UIImage *)imageNamed:(NSString *)name {
  82. return [self imageNamed:name decodeDecision:nil];
  83. }
  84. + (__kindof UIImage *)imageNamed:(NSString *)name decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  85. if (name.length == 0) return nil;
  86. if ([name hasSuffix:@"/"]) return nil;
  87. NSString *res = name.stringByDeletingPathExtension;
  88. NSString *ext = name.pathExtension;
  89. NSString *path = nil;
  90. CGFloat scale = 1;
  91. // If no extension, guess by system supported (same as UIImage).
  92. NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
  93. NSArray *scales = _NSBundlePreferredScales();
  94. for (int s = 0; s < scales.count; s++) {
  95. scale = ((NSNumber *)scales[s]).floatValue;
  96. NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
  97. for (NSString *e in exts) {
  98. path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
  99. if (path) break;
  100. }
  101. if (path) break;
  102. }
  103. if (path.length == 0) {
  104. // 🙄波儿菜:Assets.xcassets supported.
  105. return [super imageNamed:name];
  106. }
  107. NSData *data = [NSData dataWithContentsOfFile:path];
  108. if (data.length == 0) return nil;
  109. return [[self alloc] initWithData:data scale:scale decodeDecision:decodeDecision];
  110. }
  111. + (YBImage *)imageWithContentsOfFile:(NSString *)path {
  112. return [self imageWithContentsOfFile:path decodeDecision:nil];
  113. }
  114. + (YBImage *)imageWithContentsOfFile:(NSString *)path decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  115. return [[self alloc] initWithContentsOfFile:path decodeDecision:decodeDecision];
  116. }
  117. + (YBImage *)imageWithData:(NSData *)data {
  118. return [self imageWithData:data decodeDecision:nil];
  119. }
  120. + (YBImage *)imageWithData:(NSData *)data decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  121. return [[self alloc] initWithData:data decodeDecision:decodeDecision];
  122. }
  123. + (YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale {
  124. return [self imageWithData:data scale:scale decodeDecision:nil];
  125. }
  126. + (YBImage *)imageWithData:(NSData *)data scale:(CGFloat)scale decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  127. return [[self alloc] initWithData:data scale:scale decodeDecision:decodeDecision];
  128. }
  129. - (instancetype)initWithContentsOfFile:(NSString *)path {
  130. return [self initWithContentsOfFile:path decodeDecision:nil];
  131. }
  132. - (instancetype)initWithContentsOfFile:(NSString *)path decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  133. NSData *data = [NSData dataWithContentsOfFile:path];
  134. return [self initWithData:data scale:_NSStringPathScale(path) decodeDecision:decodeDecision];
  135. }
  136. - (instancetype)initWithData:(NSData *)data {
  137. return [self initWithData:data decodeDecision:nil];
  138. }
  139. - (instancetype)initWithData:(NSData *)data decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  140. return [self initWithData:data scale:1 decodeDecision:decodeDecision];
  141. }
  142. - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
  143. return [self initWithData:data scale:scale decodeDecision:nil];
  144. }
  145. - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale decodeDecision:(nullable YBImageDecodeDecision)decodeDecision {
  146. if (data.length == 0) return nil;
  147. if (scale <= 0) scale = [UIScreen mainScreen].scale;
  148. _preloadedLock = dispatch_semaphore_create(1);
  149. @autoreleasepool {
  150. YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
  151. // 🙄波儿菜:Determine whether should to decode.
  152. BOOL decodeForDisplay = YES;
  153. if (decodeDecision) {
  154. decodeForDisplay = decodeDecision(CGSizeMake(decoder.width, decoder.height), decoder.scale ?: 1);
  155. }
  156. YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:decodeForDisplay];
  157. UIImage *image = frame.image;
  158. if (!image) return nil;
  159. self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
  160. if (!self) return nil;
  161. _animatedImageType = decoder.type;
  162. if (decoder.frameCount > 1) {
  163. _decoder = decoder;
  164. _bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
  165. _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
  166. }
  167. self.isDecodedForDisplay = YES;
  168. }
  169. return self;
  170. }
  171. - (NSData *)animatedImageData {
  172. return _decoder.data;
  173. }
  174. - (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames {
  175. if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) {
  176. if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) {
  177. NSMutableArray *frames = [NSMutableArray new];
  178. for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) {
  179. UIImage *img = [self animatedImageFrameAtIndex:i];
  180. if (img) {
  181. [frames addObject:img];
  182. } else {
  183. [frames addObject:[NSNull null]];
  184. }
  185. }
  186. dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
  187. _preloadedFrames = frames;
  188. dispatch_semaphore_signal(_preloadedLock);
  189. } else {
  190. dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
  191. _preloadedFrames = nil;
  192. dispatch_semaphore_signal(_preloadedLock);
  193. }
  194. }
  195. }
  196. #pragma mark - protocol NSCoding
  197. - (instancetype)initWithCoder:(NSCoder *)aDecoder {
  198. NSNumber *scale = [aDecoder decodeObjectForKey:@"YYImageScale"];
  199. NSData *data = [aDecoder decodeObjectForKey:@"YYImageData"];
  200. if (data.length) {
  201. self = [self initWithData:data scale:scale.doubleValue];
  202. } else {
  203. self = [super initWithCoder:aDecoder];
  204. }
  205. return self;
  206. }
  207. - (void)encodeWithCoder:(NSCoder *)aCoder {
  208. if (_decoder.data.length) {
  209. [aCoder encodeObject:@(self.scale) forKey:@"YYImageScale"];
  210. [aCoder encodeObject:_decoder.data forKey:@"YYImageData"];
  211. } else {
  212. [super encodeWithCoder:aCoder]; // Apple use UIImagePNGRepresentation() to encode UIImage.
  213. }
  214. }
  215. #pragma mark - protocol YYAnimatedImage
  216. - (NSUInteger)animatedImageFrameCount {
  217. return _decoder.frameCount;
  218. }
  219. - (NSUInteger)animatedImageLoopCount {
  220. return _decoder.loopCount;
  221. }
  222. - (NSUInteger)animatedImageBytesPerFrame {
  223. return _bytesPerFrame;
  224. }
  225. - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
  226. if (index >= _decoder.frameCount) return nil;
  227. dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
  228. UIImage *image = _preloadedFrames[index];
  229. dispatch_semaphore_signal(_preloadedLock);
  230. if (image) return image == (id)[NSNull null] ? nil : image;
  231. return [_decoder frameAtIndex:index decodeForDisplay:YES].image;
  232. }
  233. - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
  234. NSTimeInterval duration = [_decoder frameDurationAtIndex:index];
  235. /*
  236. http://opensource.apple.com/source/WebCore/WebCore-7600.1.25/platform/graphics/cg/ImageSourceCG.cpp
  237. Many annoying ads specify a 0 duration to make an image flash as quickly as
  238. possible. We follow Safari and Firefox's behavior and use a duration of 100 ms
  239. for any frames that specify a duration of <= 10 ms.
  240. See <rdar://problem/7689300> and <http://webkit.org/b/36082> for more information.
  241. See also: http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser.
  242. */
  243. if (duration < 0.011f) return 0.100f;
  244. return duration;
  245. }
  246. @end