UIImage+WebP.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #ifdef SD_WEBP
  9. #import "UIImage+WebP.h"
  10. #import "NSImage+WebCache.h"
  11. #if __has_include(<webp/decode.h>) && __has_include(<webp/mux_types.h>) && __has_include(<webp/demux.h>)
  12. #import <webp/decode.h>
  13. #import <webp/mux_types.h>
  14. #import <webp/demux.h>
  15. #else
  16. #import "webp/decode.h"
  17. #import "webp/mux_types.h"
  18. #import "webp/demux.h"
  19. #endif
  20. #import "objc/runtime.h"
  21. // Callback for CGDataProviderRelease
  22. static void FreeImageData(void *info, const void *data, size_t size) {
  23. free((void *)data);
  24. }
  25. @implementation UIImage (WebP)
  26. - (NSInteger)sd_webpLoopCount
  27. {
  28. NSNumber *value = objc_getAssociatedObject(self, @selector(sd_webpLoopCount));
  29. return value.integerValue;
  30. }
  31. + (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data {
  32. if (!data) {
  33. return nil;
  34. }
  35. WebPData webpData;
  36. WebPDataInit(&webpData);
  37. webpData.bytes = data.bytes;
  38. webpData.size = data.length;
  39. WebPDemuxer *demuxer = WebPDemux(&webpData);
  40. if (!demuxer) {
  41. return nil;
  42. }
  43. uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);
  44. #if SD_UIKIT || SD_WATCH
  45. int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);
  46. int frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT);
  47. #endif
  48. int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
  49. int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
  50. CGBitmapInfo bitmapInfo;
  51. if (!(flags & ALPHA_FLAG)) {
  52. bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
  53. } else {
  54. bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
  55. }
  56. CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);
  57. if (!canvas) {
  58. WebPDemuxDelete(demuxer);
  59. return nil;
  60. }
  61. if (!(flags & ANIMATION_FLAG)) {
  62. // for static single webp image
  63. UIImage *staticImage = [self sd_rawWebpImageWithData:webpData];
  64. if (staticImage) {
  65. // draw on CGBitmapContext can reduce memory usage
  66. CGImageRef imageRef = staticImage.CGImage;
  67. size_t width = CGImageGetWidth(imageRef);
  68. size_t height = CGImageGetHeight(imageRef);
  69. CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef);
  70. CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
  71. #if SD_UIKIT || SD_WATCH
  72. staticImage = [[UIImage alloc] initWithCGImage:newImageRef];
  73. #else
  74. staticImage = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
  75. #endif
  76. CGImageRelease(newImageRef);
  77. }
  78. WebPDemuxDelete(demuxer);
  79. CGContextRelease(canvas);
  80. return staticImage;
  81. }
  82. // for animated webp image
  83. WebPIterator iter;
  84. if (!WebPDemuxGetFrame(demuxer, 1, &iter)) {
  85. WebPDemuxReleaseIterator(&iter);
  86. WebPDemuxDelete(demuxer);
  87. CGContextRelease(canvas);
  88. return nil;
  89. }
  90. NSMutableArray<UIImage *> *images = [NSMutableArray array];
  91. #if SD_UIKIT || SD_WATCH
  92. NSTimeInterval totalDuration = 0;
  93. int durations[frameCount];
  94. #endif
  95. do {
  96. @autoreleasepool {
  97. UIImage *image;
  98. if (iter.blend_method == WEBP_MUX_BLEND) {
  99. image = [self sd_blendWebpImageWithCanvas:canvas iterator:iter];
  100. } else {
  101. image = [self sd_nonblendWebpImageWithCanvas:canvas iterator:iter];
  102. }
  103. if (!image) {
  104. continue;
  105. }
  106. [images addObject:image];
  107. #if SD_MAC
  108. break;
  109. #else
  110. int duration = iter.duration;
  111. if (duration <= 10) {
  112. // WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
  113. // Some animated WebP images also created without duration, we should keep compatibility
  114. duration = 100;
  115. }
  116. totalDuration += duration;
  117. size_t count = images.count;
  118. durations[count - 1] = duration;
  119. #endif
  120. }
  121. } while (WebPDemuxNextFrame(&iter));
  122. WebPDemuxReleaseIterator(&iter);
  123. WebPDemuxDelete(demuxer);
  124. CGContextRelease(canvas);
  125. UIImage *finalImage = nil;
  126. #if SD_UIKIT || SD_WATCH
  127. NSArray<UIImage *> *animatedImages = [self sd_animatedImagesWithImages:images durations:durations totalDuration:totalDuration];
  128. finalImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.0];
  129. if (finalImage) {
  130. objc_setAssociatedObject(finalImage, @selector(sd_webpLoopCount), @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  131. }
  132. #elif SD_MAC
  133. finalImage = images.firstObject;
  134. #endif
  135. return finalImage;
  136. }
  137. + (nullable UIImage *)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
  138. UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
  139. if (!image) {
  140. return nil;
  141. }
  142. size_t canvasWidth = CGBitmapContextGetWidth(canvas);
  143. size_t canvasHeight = CGBitmapContextGetHeight(canvas);
  144. CGSize size = CGSizeMake(canvasWidth, canvasHeight);
  145. CGFloat tmpX = iter.x_offset;
  146. CGFloat tmpY = size.height - iter.height - iter.y_offset;
  147. CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
  148. CGContextDrawImage(canvas, imageRect, image.CGImage);
  149. CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
  150. #if SD_UIKIT || SD_WATCH
  151. image = [UIImage imageWithCGImage:newImageRef];
  152. #elif SD_MAC
  153. image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
  154. #endif
  155. CGImageRelease(newImageRef);
  156. if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
  157. CGContextClearRect(canvas, imageRect);
  158. }
  159. return image;
  160. }
  161. + (nullable UIImage *)sd_nonblendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
  162. UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
  163. if (!image) {
  164. return nil;
  165. }
  166. size_t canvasWidth = CGBitmapContextGetWidth(canvas);
  167. size_t canvasHeight = CGBitmapContextGetHeight(canvas);
  168. CGSize size = CGSizeMake(canvasWidth, canvasHeight);
  169. CGFloat tmpX = iter.x_offset;
  170. CGFloat tmpY = size.height - iter.height - iter.y_offset;
  171. CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
  172. CGContextClearRect(canvas, imageRect);
  173. CGContextDrawImage(canvas, imageRect, image.CGImage);
  174. CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
  175. #if SD_UIKIT || SD_WATCH
  176. image = [UIImage imageWithCGImage:newImageRef];
  177. #elif SD_MAC
  178. image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
  179. #endif
  180. CGImageRelease(newImageRef);
  181. if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
  182. CGContextClearRect(canvas, imageRect);
  183. }
  184. return image;
  185. }
  186. + (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData {
  187. WebPDecoderConfig config;
  188. if (!WebPInitDecoderConfig(&config)) {
  189. return nil;
  190. }
  191. if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) {
  192. return nil;
  193. }
  194. config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB;
  195. config.options.use_threads = 1;
  196. // Decode the WebP image data into a RGBA value array
  197. if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
  198. return nil;
  199. }
  200. int width = config.input.width;
  201. int height = config.input.height;
  202. if (config.options.use_scaling) {
  203. width = config.options.scaled_width;
  204. height = config.options.scaled_height;
  205. }
  206. // Construct a UIImage from the decoded RGBA value array
  207. CGDataProviderRef provider =
  208. CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
  209. CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB();
  210. CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
  211. size_t components = config.input.has_alpha ? 4 : 3;
  212. CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
  213. CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
  214. CGDataProviderRelease(provider);
  215. #if SD_UIKIT || SD_WATCH
  216. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
  217. #else
  218. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef size:NSZeroSize];
  219. #endif
  220. CGImageRelease(imageRef);
  221. return image;
  222. }
  223. + (NSArray<UIImage *> *)sd_animatedImagesWithImages:(NSArray<UIImage *> *)images durations:(int const * const)durations totalDuration:(NSTimeInterval)totalDuration
  224. {
  225. // [UIImage animatedImageWithImages:duration:] only use the average duration for per frame
  226. // divide the total duration to implement per frame duration for animated WebP
  227. NSUInteger count = images.count;
  228. if (!count) {
  229. return nil;
  230. }
  231. if (count == 1) {
  232. return images;
  233. }
  234. int const gcd = gcdArray(count, durations);
  235. NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:count];
  236. [images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
  237. int duration = durations[idx];
  238. int repeatCount;
  239. if (gcd) {
  240. repeatCount = duration / gcd;
  241. } else {
  242. repeatCount = 1;
  243. }
  244. for (int i = 0; i < repeatCount; ++i) {
  245. [animatedImages addObject:image];
  246. }
  247. }];
  248. return animatedImages;
  249. }
  250. static CGColorSpaceRef SDCGColorSpaceGetDeviceRGB() {
  251. static CGColorSpaceRef space;
  252. static dispatch_once_t onceToken;
  253. dispatch_once(&onceToken, ^{
  254. space = CGColorSpaceCreateDeviceRGB();
  255. });
  256. return space;
  257. }
  258. static int gcdArray(size_t const count, int const * const values) {
  259. int result = values[0];
  260. for (size_t i = 1; i < count; ++i) {
  261. result = gcd(values[i], result);
  262. }
  263. return result;
  264. }
  265. static int gcd(int a,int b) {
  266. int c;
  267. while (a != 0) {
  268. c = a;
  269. a = b % a;
  270. b = c;
  271. }
  272. return b;
  273. }
  274. @end
  275. #endif