RCTImageUtils.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /*
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. #import <React/RCTImageUtils.h>
  8. #import <tgmath.h>
  9. #import <ImageIO/ImageIO.h>
  10. #import <MobileCoreServices/UTCoreTypes.h>
  11. #import <React/RCTLog.h>
  12. #import <React/RCTUtils.h>
  13. static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
  14. {
  15. return ceil(value * scale) / scale;
  16. }
  17. static CGFloat RCTFloorValue(CGFloat value, CGFloat scale)
  18. {
  19. return floor(value * scale) / scale;
  20. }
  21. static CGSize RCTCeilSize(CGSize size, CGFloat scale)
  22. {
  23. return (CGSize){
  24. RCTCeilValue(size.width, scale),
  25. RCTCeilValue(size.height, scale)
  26. };
  27. }
  28. static CGImagePropertyOrientation CGImagePropertyOrientationFromUIImageOrientation(UIImageOrientation imageOrientation)
  29. {
  30. // see https://stackoverflow.com/a/6699649/496389
  31. switch (imageOrientation) {
  32. case UIImageOrientationUp: return kCGImagePropertyOrientationUp;
  33. case UIImageOrientationDown: return kCGImagePropertyOrientationDown;
  34. case UIImageOrientationLeft: return kCGImagePropertyOrientationLeft;
  35. case UIImageOrientationRight: return kCGImagePropertyOrientationRight;
  36. case UIImageOrientationUpMirrored: return kCGImagePropertyOrientationUpMirrored;
  37. case UIImageOrientationDownMirrored: return kCGImagePropertyOrientationDownMirrored;
  38. case UIImageOrientationLeftMirrored: return kCGImagePropertyOrientationLeftMirrored;
  39. case UIImageOrientationRightMirrored: return kCGImagePropertyOrientationRightMirrored;
  40. default: return kCGImagePropertyOrientationUp;
  41. }
  42. }
  43. CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize,
  44. CGFloat destScale, RCTResizeMode resizeMode)
  45. {
  46. if (CGSizeEqualToSize(destSize, CGSizeZero)) {
  47. // Assume we require the largest size available
  48. return (CGRect){CGPointZero, sourceSize};
  49. }
  50. CGFloat aspect = sourceSize.width / sourceSize.height;
  51. // If only one dimension in destSize is non-zero (for example, an Image
  52. // with `flex: 1` whose height is indeterminate), calculate the unknown
  53. // dimension based on the aspect ratio of sourceSize
  54. if (destSize.width == 0) {
  55. destSize.width = destSize.height * aspect;
  56. }
  57. if (destSize.height == 0) {
  58. destSize.height = destSize.width / aspect;
  59. }
  60. // Calculate target aspect ratio if needed
  61. CGFloat targetAspect = 0.0;
  62. if (resizeMode != RCTResizeModeCenter &&
  63. resizeMode != RCTResizeModeStretch) {
  64. targetAspect = destSize.width / destSize.height;
  65. if (aspect == targetAspect) {
  66. resizeMode = RCTResizeModeStretch;
  67. }
  68. }
  69. switch (resizeMode) {
  70. case RCTResizeModeStretch:
  71. case RCTResizeModeRepeat:
  72. return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)};
  73. case RCTResizeModeContain:
  74. if (targetAspect <= aspect) { // target is taller than content
  75. sourceSize.width = destSize.width;
  76. sourceSize.height = sourceSize.width / aspect;
  77. } else { // target is wider than content
  78. sourceSize.height = destSize.height;
  79. sourceSize.width = sourceSize.height * aspect;
  80. }
  81. return (CGRect){
  82. {
  83. RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
  84. RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
  85. },
  86. RCTCeilSize(sourceSize, destScale)
  87. };
  88. case RCTResizeModeCover:
  89. if (targetAspect <= aspect) { // target is taller than content
  90. sourceSize.height = destSize.height;
  91. sourceSize.width = sourceSize.height * aspect;
  92. destSize.width = destSize.height * targetAspect;
  93. return (CGRect){
  94. {RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), 0},
  95. RCTCeilSize(sourceSize, destScale)
  96. };
  97. } else { // target is wider than content
  98. sourceSize.width = destSize.width;
  99. sourceSize.height = sourceSize.width / aspect;
  100. destSize.height = destSize.width / targetAspect;
  101. return (CGRect){
  102. {0, RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale)},
  103. RCTCeilSize(sourceSize, destScale)
  104. };
  105. }
  106. case RCTResizeModeCenter:
  107. // Make sure the image is not clipped by the target.
  108. if (sourceSize.height > destSize.height) {
  109. sourceSize.width = destSize.width;
  110. sourceSize.height = sourceSize.width / aspect;
  111. }
  112. if (sourceSize.width > destSize.width) {
  113. sourceSize.height = destSize.height;
  114. sourceSize.width = sourceSize.height * aspect;
  115. }
  116. return (CGRect){
  117. {
  118. RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
  119. RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
  120. },
  121. RCTCeilSize(sourceSize, destScale)
  122. };
  123. }
  124. }
  125. CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize, CGRect targetRect)
  126. {
  127. CGAffineTransform transform = CGAffineTransformIdentity;
  128. transform = CGAffineTransformTranslate(transform,
  129. targetRect.origin.x,
  130. targetRect.origin.y);
  131. transform = CGAffineTransformScale(transform,
  132. targetRect.size.width / sourceSize.width,
  133. targetRect.size.height / sourceSize.height);
  134. return transform;
  135. }
  136. CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale,
  137. CGSize destSize, CGFloat destScale,
  138. RCTResizeMode resizeMode,
  139. BOOL allowUpscaling)
  140. {
  141. switch (resizeMode) {
  142. case RCTResizeModeCenter:
  143. return RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
  144. case RCTResizeModeStretch:
  145. if (!allowUpscaling) {
  146. CGFloat scale = sourceScale / destScale;
  147. destSize.width = MIN(sourceSize.width * scale, destSize.width);
  148. destSize.height = MIN(sourceSize.height * scale, destSize.height);
  149. }
  150. return RCTCeilSize(destSize, destScale);
  151. default: {
  152. // Get target size
  153. CGSize size = RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
  154. if (!allowUpscaling) {
  155. // return sourceSize if target size is larger
  156. if (sourceSize.width * sourceScale < size.width * destScale) {
  157. return sourceSize;
  158. }
  159. }
  160. return size;
  161. }
  162. }
  163. }
  164. BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale,
  165. CGSize destSize, CGFloat destScale,
  166. RCTResizeMode resizeMode)
  167. {
  168. if (CGSizeEqualToSize(destSize, CGSizeZero)) {
  169. // Assume we require the largest size available
  170. return YES;
  171. }
  172. // Precompensate for scale
  173. CGFloat scale = sourceScale / destScale;
  174. sourceSize.width *= scale;
  175. sourceSize.height *= scale;
  176. // Calculate aspect ratios if needed (don't bother if resizeMode == stretch)
  177. CGFloat aspect = 0.0, targetAspect = 0.0;
  178. if (resizeMode != UIViewContentModeScaleToFill) {
  179. aspect = sourceSize.width / sourceSize.height;
  180. targetAspect = destSize.width / destSize.height;
  181. if (aspect == targetAspect) {
  182. resizeMode = RCTResizeModeStretch;
  183. }
  184. }
  185. switch (resizeMode) {
  186. case RCTResizeModeStretch:
  187. return destSize.width > sourceSize.width || destSize.height > sourceSize.height;
  188. case RCTResizeModeContain:
  189. if (targetAspect <= aspect) { // target is taller than content
  190. return destSize.width > sourceSize.width;
  191. } else { // target is wider than content
  192. return destSize.height > sourceSize.height;
  193. }
  194. case RCTResizeModeCover:
  195. if (targetAspect <= aspect) { // target is taller than content
  196. return destSize.height > sourceSize.height;
  197. } else { // target is wider than content
  198. return destSize.width > sourceSize.width;
  199. }
  200. case RCTResizeModeRepeat:
  201. case RCTResizeModeCenter:
  202. return NO;
  203. }
  204. }
  205. UIImage *__nullable RCTDecodeImageWithData(NSData *data,
  206. CGSize destSize,
  207. CGFloat destScale,
  208. RCTResizeMode resizeMode)
  209. {
  210. CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  211. if (!sourceRef) {
  212. return nil;
  213. }
  214. // Get original image size
  215. CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
  216. if (!imageProperties) {
  217. CFRelease(sourceRef);
  218. return nil;
  219. }
  220. NSNumber *width = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
  221. NSNumber *height = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight);
  222. CGSize sourceSize = {width.doubleValue, height.doubleValue};
  223. CFRelease(imageProperties);
  224. if (CGSizeEqualToSize(destSize, CGSizeZero)) {
  225. destSize = sourceSize;
  226. if (!destScale) {
  227. destScale = 1;
  228. }
  229. } else if (!destScale) {
  230. destScale = RCTScreenScale();
  231. }
  232. if (resizeMode == UIViewContentModeScaleToFill) {
  233. // Decoder cannot change aspect ratio, so RCTResizeModeStretch is equivalent
  234. // to RCTResizeModeCover for our purposes
  235. resizeMode = RCTResizeModeCover;
  236. }
  237. // Calculate target size
  238. CGSize targetSize = RCTTargetSize(sourceSize, 1, destSize, destScale, resizeMode, NO);
  239. CGSize targetPixelSize = RCTSizeInPixels(targetSize, destScale);
  240. CGFloat maxPixelSize = fmax(fmin(sourceSize.width, targetPixelSize.width),
  241. fmin(sourceSize.height, targetPixelSize.height));
  242. NSDictionary<NSString *, NSNumber *> *options = @{
  243. (id)kCGImageSourceShouldAllowFloat: @YES,
  244. (id)kCGImageSourceCreateThumbnailWithTransform: @YES,
  245. (id)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
  246. (id)kCGImageSourceThumbnailMaxPixelSize: @(maxPixelSize),
  247. };
  248. // Get thumbnail
  249. CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
  250. CFRelease(sourceRef);
  251. if (!imageRef) {
  252. return nil;
  253. }
  254. // Return image
  255. UIImage *image = [UIImage imageWithCGImage:imageRef
  256. scale:destScale
  257. orientation:UIImageOrientationUp];
  258. CGImageRelease(imageRef);
  259. return image;
  260. }
  261. NSDictionary<NSString *, id> *__nullable RCTGetImageMetadata(NSData *data)
  262. {
  263. CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  264. if (!sourceRef) {
  265. return nil;
  266. }
  267. CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
  268. CFRelease(sourceRef);
  269. return (__bridge_transfer id)imageProperties;
  270. }
  271. NSData *__nullable RCTGetImageData(UIImage *image, float quality)
  272. {
  273. CGImageRef cgImage = image.CGImage;
  274. if (!cgImage) {
  275. return NULL;
  276. }
  277. NSMutableDictionary *properties = [[NSMutableDictionary alloc] initWithDictionary:@{
  278. (id)kCGImagePropertyOrientation : @(CGImagePropertyOrientationFromUIImageOrientation(image.imageOrientation))
  279. }];
  280. CGImageDestinationRef destination;
  281. CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0);
  282. if (RCTImageHasAlpha(cgImage)) {
  283. // get png data
  284. destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL);
  285. } else {
  286. // get jpeg data
  287. destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL);
  288. [properties setValue:@(quality) forKey:(id)kCGImageDestinationLossyCompressionQuality];
  289. }
  290. if (!destination) {
  291. CFRelease(imageData);
  292. return NULL;
  293. }
  294. CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
  295. if (!CGImageDestinationFinalize(destination)) {
  296. CFRelease(imageData);
  297. imageData = NULL;
  298. }
  299. CFRelease(destination);
  300. return (__bridge_transfer NSData *)imageData;
  301. }
  302. UIImage *__nullable RCTTransformImage(UIImage *image,
  303. CGSize destSize,
  304. CGFloat destScale,
  305. CGAffineTransform transform)
  306. {
  307. if (destSize.width <= 0 | destSize.height <= 0 || destScale <= 0) {
  308. return nil;
  309. }
  310. BOOL opaque = !RCTImageHasAlpha(image.CGImage);
  311. UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
  312. CGContextRef currentContext = UIGraphicsGetCurrentContext();
  313. CGContextConcatCTM(currentContext, transform);
  314. [image drawAtPoint:CGPointZero];
  315. UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
  316. UIGraphicsEndImageContext();
  317. return result;
  318. }
  319. BOOL RCTImageHasAlpha(CGImageRef image)
  320. {
  321. switch (CGImageGetAlphaInfo(image)) {
  322. case kCGImageAlphaNone:
  323. case kCGImageAlphaNoneSkipLast:
  324. case kCGImageAlphaNoneSkipFirst:
  325. return NO;
  326. default:
  327. return YES;
  328. }
  329. }