ImageResizer.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #import "ImageResizer.h"
  2. #import <React/RCTLog.h>
  3. #import <AssetsLibrary/AssetsLibrary.h>
  4. #import <MobileCoreServices/MobileCoreServices.h>
  5. #if __has_include(<React/RCTLog.h>)
  6. #import <React/RCTLog.h>
  7. #import <React/RCTImageLoader.h>
  8. #else
  9. #import "RCTLog.h"
  10. #import "RCTImageLoader.h"
  11. #endif
  12. NSString *moduleName = @"ImageResizer";
  13. @implementation ImageResizer
  14. @synthesize bridge = _bridge;
  15. RCT_EXPORT_MODULE()
  16. RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
  17. {
  18. [self createResizedImage:uri width:width height:height format:format quality:quality mode:mode onlyScaleDown:onlyScaleDown rotation:rotation outputPath:outputPath keepMeta:keepMeta resolve:resolve reject:reject];
  19. }
  20. - (void)createResizedImage:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  21. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  22. @try {
  23. CGSize newSize = CGSizeMake(width, height);
  24. //Set image extension
  25. NSString *extension = @"jpg";
  26. if ([format isEqualToString:@"PNG"]) {
  27. extension = @"png";
  28. }
  29. NSString* fullPath;
  30. @try {
  31. fullPath = generateFilePath(extension, outputPath);
  32. } @catch (NSException *exception) {
  33. [NSException raise:moduleName format:@"Invalid output path."];
  34. }
  35. RCTImageLoader *loader = [self.bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES];
  36. NSURLRequest *request = [RCTConvert NSURLRequest:uri];
  37. [loader loadImageWithURLRequest:request
  38. size:newSize
  39. scale:1
  40. clipped:NO
  41. resizeMode:RCTResizeModeContain
  42. progressBlock:nil
  43. partialLoadBlock:nil
  44. completionBlock:^(NSError *error, UIImage *image) {
  45. if (error) {
  46. RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", [NSString stringWithFormat: @"%ld", (long)error.code], error.description]);
  47. reject([NSString stringWithFormat: @"%ld", (long)error.code], error.description, nil);
  48. return;
  49. }
  50. NSDictionary * response = transformImage(image, uri, [rotation integerValue], newSize, fullPath, format, (int)quality, [keepMeta boolValue], @{@"mode": mode, @"onlyScaleDown": [NSNumber numberWithBool:onlyScaleDown]});
  51. resolve(response);
  52. }];
  53. } @catch (NSException *exception) {
  54. RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", exception.name, exception.reason]);
  55. reject(exception.name, exception.reason, nil);
  56. }
  57. });
  58. }
  59. bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality, NSMutableDictionary *metadata)
  60. {
  61. if(metadata == nil){
  62. NSData* data = nil;
  63. if ([format isEqualToString:@"JPEG"]) {
  64. data = UIImageJPEGRepresentation(image, quality / 100.0);
  65. } else if ([format isEqualToString:@"PNG"]) {
  66. data = UIImagePNGRepresentation(image);
  67. }
  68. if (data == nil) {
  69. return NO;
  70. }
  71. NSFileManager* fileManager = [NSFileManager defaultManager];
  72. return [fileManager createFileAtPath:fullPath contents:data attributes:nil];
  73. }
  74. // process / write metadata together with image data
  75. else{
  76. CFStringRef imgType = kUTTypeJPEG;
  77. if ([format isEqualToString:@"JPEG"]) {
  78. [metadata setObject:@(quality / 100.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
  79. }
  80. else if([format isEqualToString:@"PNG"]){
  81. imgType = kUTTypePNG;
  82. }
  83. else{
  84. return NO;
  85. }
  86. NSMutableData * destData = [NSMutableData data];
  87. CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)destData, imgType, 1, NULL);
  88. @try{
  89. CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef) metadata);
  90. // write final image data with metadata to our destination
  91. if (CGImageDestinationFinalize(destination)){
  92. NSFileManager* fileManager = [NSFileManager defaultManager];
  93. return [fileManager createFileAtPath:fullPath contents:destData attributes:nil];
  94. }
  95. else{
  96. return NO;
  97. }
  98. }
  99. @finally{
  100. @try{
  101. CFRelease(destination);
  102. }
  103. @catch(NSException *exception){
  104. NSLog(@"Failed to release CGImageDestinationRef: %@", exception);
  105. }
  106. }
  107. }
  108. }
  109. NSString * generateFilePath(NSString * ext, NSString * outputPath)
  110. {
  111. NSString* directory;
  112. if ([outputPath length] == 0) {
  113. NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  114. directory = [paths firstObject];
  115. } else {
  116. NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  117. NSString *documentsDirectory = [paths objectAtIndex:0];
  118. if ([outputPath hasPrefix:documentsDirectory]) {
  119. directory = outputPath;
  120. } else {
  121. directory = [documentsDirectory stringByAppendingPathComponent:outputPath];
  122. }
  123. NSError *error;
  124. [[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error];
  125. if (error) {
  126. NSLog(@"Error creating documents subdirectory: %@", error);
  127. @throw [NSException exceptionWithName:@"InvalidPathException" reason:[NSString stringWithFormat:@"Error creating documents subdirectory: %@", error] userInfo:nil];
  128. }
  129. }
  130. NSString* name = [[NSUUID UUID] UUIDString];
  131. NSString* fullName = [NSString stringWithFormat:@"%@.%@", name, ext];
  132. NSString* fullPath = [directory stringByAppendingPathComponent:fullName];
  133. return fullPath;
  134. }
  135. UIImage * rotateImage(UIImage *inputImage, float rotationDegrees)
  136. {
  137. // We want only fixed 0, 90, 180, 270 degree rotations.
  138. const int rotDiv90 = (int)round(rotationDegrees / 90);
  139. const int rotQuadrant = rotDiv90 % 4;
  140. const int rotQuadrantAbs = (rotQuadrant < 0) ? rotQuadrant + 4 : rotQuadrant;
  141. // Return the input image if no rotation specified.
  142. if (0 == rotQuadrantAbs) {
  143. return inputImage;
  144. } else {
  145. // Rotate the image by 80, 180, 270.
  146. UIImageOrientation orientation = UIImageOrientationUp;
  147. switch(rotQuadrantAbs) {
  148. case 1:
  149. orientation = UIImageOrientationRight; // 90 deg CW
  150. break;
  151. case 2:
  152. orientation = UIImageOrientationDown; // 180 deg rotation
  153. break;
  154. default:
  155. orientation = UIImageOrientationLeft; // 90 deg CCW
  156. break;
  157. }
  158. return [[UIImage alloc] initWithCGImage: inputImage.CGImage
  159. scale: 1.0
  160. orientation: orientation];
  161. }
  162. }
  163. float getScaleForProportionalResize(CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize)
  164. {
  165. float sx = theSize.width;
  166. float sy = theSize.height;
  167. float dx = intoSize.width;
  168. float dy = intoSize.height;
  169. float scale = 1;
  170. if( sx != 0 && sy != 0 )
  171. {
  172. dx = dx / sx;
  173. dy = dy / sy;
  174. // if maximize is true, take LARGER of the scales, else smaller
  175. if (maximize) {
  176. scale = MAX(dx, dy);
  177. } else {
  178. scale = MIN(dx, dy);
  179. }
  180. if (onlyScaleDown) {
  181. scale = MIN(scale, 1);
  182. }
  183. }
  184. else
  185. {
  186. scale = 0;
  187. }
  188. return scale;
  189. }
  190. // returns a resized image keeping aspect ratio and considering
  191. // any :image scale factor.
  192. // The returned image is an unscaled image (scale = 1.0)
  193. // so no additional scaling math needs to be done to get its pixel dimensions
  194. UIImage* scaleImage (UIImage* image, CGSize toSize, NSString* mode, bool onlyScaleDown)
  195. {
  196. // Need to do scaling corrections
  197. // based on scale, since UIImage width/height gives us
  198. // a possibly scaled image (dimensions in points)
  199. // Idea taken from RNCamera resize code
  200. CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
  201. // using this instead of ImageHelpers allows us to consider
  202. // rotation variations
  203. CGSize newSize;
  204. if ([mode isEqualToString:@"stretch"]) {
  205. // Distort aspect ratio
  206. int width = toSize.width;
  207. int height = toSize.height;
  208. if (onlyScaleDown) {
  209. width = MIN(width, imageSize.width);
  210. height = MIN(height, imageSize.height);
  211. }
  212. newSize = CGSizeMake(width, height);
  213. } else {
  214. // Either "contain" (default) or "cover": preserve aspect ratio
  215. bool maximize = [mode isEqualToString:@"cover"];
  216. float scale = getScaleForProportionalResize(imageSize, toSize, onlyScaleDown, maximize);
  217. newSize = CGSizeMake(roundf(imageSize.width * scale), roundf(imageSize.height * scale));
  218. }
  219. UIGraphicsBeginImageContextWithOptions(newSize, NO, 1.0);
  220. [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
  221. UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
  222. UIGraphicsEndImageContext();
  223. return newImage;
  224. }
  225. // Returns the image's metadata, or nil if failed to retrieve it.
  226. NSMutableDictionary * getImageMeta(NSString * path)
  227. {
  228. if([path hasPrefix:@"assets-library"]) {
  229. __block NSMutableDictionary* res = nil;
  230. ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
  231. {
  232. NSDictionary *exif = [[myasset defaultRepresentation] metadata];
  233. res = [exif mutableCopy];
  234. };
  235. ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
  236. NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
  237. [assetslibrary assetForURL:url resultBlock:resultblock failureBlock:^(NSError *error) { NSLog(@"error couldn't image from assets library"); }];
  238. return res;
  239. } else {
  240. NSData* imageData = nil;
  241. if ([path hasPrefix:@"data:"] || [path hasPrefix:@"file:"]) {
  242. NSURL *imageUrl = [[NSURL alloc] initWithString:path];
  243. imageData = [NSData dataWithContentsOfURL:imageUrl];
  244. } else {
  245. imageData = [NSData dataWithContentsOfFile:path];
  246. }
  247. if(imageData == nil){
  248. NSLog(@"Could not get image file data to extract metadata.");
  249. return nil;
  250. }
  251. CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
  252. if(source != nil){
  253. CFDictionaryRef metaRef = CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
  254. // release CF image
  255. CFRelease(source);
  256. CFMutableDictionaryRef metaRefMutable = CFDictionaryCreateMutableCopy(NULL, 0, metaRef);
  257. // release the source meta ref now that we've copie it
  258. CFRelease(metaRef);
  259. // bridge CF object so it auto releases
  260. NSMutableDictionary* res = (NSMutableDictionary *)CFBridgingRelease(metaRefMutable);
  261. return res;
  262. }
  263. else{
  264. return nil;
  265. }
  266. }
  267. }
  268. NSDictionary * transformImage(UIImage *image,
  269. NSString * originalPath,
  270. int rotation,
  271. CGSize newSize,
  272. NSString* fullPath,
  273. NSString* format,
  274. int quality,
  275. BOOL keepMeta,
  276. NSDictionary* options)
  277. {
  278. if (image == nil) {
  279. [NSException raise:moduleName format:@"Can't retrieve the file from the path."];
  280. }
  281. // Rotate image if rotation is specified.
  282. if (0 != (int)rotation) {
  283. image = rotateImage(image, rotation);
  284. if (image == nil) {
  285. [NSException raise:moduleName format:@"Can't rotate the image."];
  286. }
  287. }
  288. // Do the resizing
  289. UIImage * scaledImage = scaleImage(
  290. image,
  291. newSize,
  292. options[@"mode"],
  293. [[options objectForKey:@"onlyScaleDown"] boolValue]
  294. );
  295. if (scaledImage == nil) {
  296. [NSException raise:moduleName format:@"Can't resize the image."];
  297. }
  298. NSMutableDictionary *metadata = nil;
  299. // to be consistent with Android, we will only allow JPEG
  300. // to do this.
  301. if(keepMeta && [format isEqualToString:@"JPEG"]){
  302. metadata = getImageMeta(originalPath);
  303. // remove orientation (since we fix it)
  304. // width/height meta is adjusted automatically
  305. // NOTE: This might still leave some stale values due to resize
  306. metadata[(NSString*)kCGImagePropertyOrientation] = @(1);
  307. }
  308. // Compress and save the image
  309. if (!saveImage(fullPath, scaledImage, format, quality, metadata)) {
  310. [NSException raise:moduleName format:@"Can't save the image. Check your compression format and your output path"];
  311. }
  312. NSURL *fileUrl = [[NSURL alloc] initFileURLWithPath:fullPath];
  313. NSString *fileName = fileUrl.lastPathComponent;
  314. NSError *attributesError = nil;
  315. NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:&attributesError];
  316. NSNumber *fileSize = fileAttributes == nil ? 0 : [fileAttributes objectForKey:NSFileSize];
  317. NSDictionary *response = @{@"path": fullPath,
  318. @"uri": fileUrl.absoluteString,
  319. @"name": fileName,
  320. @"size": fileSize == nil ? @(0) : fileSize,
  321. @"width": @(scaledImage.size.width),
  322. @"height": @(scaledImage.size.height),
  323. };
  324. return response;
  325. }
  326. // Don't compile this code when we build for the old architecture.
  327. #ifdef RCT_NEW_ARCH_ENABLED
  328. - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
  329. (const facebook::react::ObjCTurboModule::InitParams &)params
  330. {
  331. return std::make_shared<facebook::react::NativeImageResizerSpecJSI>(params);
  332. }
  333. #endif
  334. @end