FBImageIOScaler.m 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * Copyright (c) 2015-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBImageIOScaler.h"
  10. #import <ImageIO/ImageIO.h>
  11. #import <UIKit/UIKit.h>
  12. @import UniformTypeIdentifiers;
  13. #import "FBConfiguration.h"
  14. #import "FBErrorBuilder.h"
  15. #import "FBLogger.h"
  16. const CGFloat FBMinScalingFactor = 0.01f;
  17. const CGFloat FBMaxScalingFactor = 1.0f;
  18. const CGFloat FBMinCompressionQuality = 0.0f;
  19. const CGFloat FBMaxCompressionQuality = 1.0f;
  20. @interface FBImageIOScaler ()
  21. @property (nonatomic) NSData *nextImage;
  22. @property (nonatomic, readonly) NSLock *nextImageLock;
  23. @property (nonatomic, readonly) dispatch_queue_t scalingQueue;
  24. @end
  25. @implementation FBImageIOScaler
  26. - (id)init
  27. {
  28. self = [super init];
  29. if (self) {
  30. _nextImageLock = [[NSLock alloc] init];
  31. _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL);
  32. }
  33. return self;
  34. }
  35. - (void)submitImage:(NSData *)image
  36. scalingFactor:(CGFloat)scalingFactor
  37. compressionQuality:(CGFloat)compressionQuality
  38. completionHandler:(void (^)(NSData *))completionHandler
  39. {
  40. [self.nextImageLock lock];
  41. if (self.nextImage != nil) {
  42. [FBLogger verboseLog:@"Discarding screenshot"];
  43. }
  44. scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor));
  45. compressionQuality = MAX(FBMinCompressionQuality, MIN(FBMaxCompressionQuality, compressionQuality));
  46. self.nextImage = image;
  47. [self.nextImageLock unlock];
  48. #pragma clang diagnostic push
  49. #pragma clang diagnostic ignored "-Wcompletion-handler"
  50. dispatch_async(self.scalingQueue, ^{
  51. [self.nextImageLock lock];
  52. NSData *next = self.nextImage;
  53. self.nextImage = nil;
  54. [self.nextImageLock unlock];
  55. if (next == nil) {
  56. return;
  57. }
  58. NSError *error;
  59. NSData *scaled = [self scaledJpegImageWithImage:next
  60. scalingFactor:scalingFactor
  61. compressionQuality:compressionQuality
  62. error:&error];
  63. if (scaled == nil) {
  64. [FBLogger logFmt:@"%@", error.description];
  65. return;
  66. }
  67. completionHandler(scaled);
  68. });
  69. #pragma clang diagnostic pop
  70. }
  71. // This method is more optimized for JPEG scaling
  72. // and should be used in `submitImage` API, while the `scaledImageWithImage`
  73. // one is more generic
  74. - (nullable NSData *)scaledJpegImageWithImage:(NSData *)image
  75. scalingFactor:(CGFloat)scalingFactor
  76. compressionQuality:(CGFloat)compressionQuality
  77. error:(NSError **)error
  78. {
  79. CGImageSourceRef imageData = CGImageSourceCreateWithData((CFDataRef)image, nil);
  80. CGSize size = [self.class imageSizeWithImage:imageData];
  81. CGFloat scaledMaxPixelSize = MAX(size.width, size.height) * scalingFactor;
  82. CFDictionaryRef params = (__bridge CFDictionaryRef)@{
  83. (const NSString *)kCGImageSourceCreateThumbnailWithTransform: @(YES),
  84. (const NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent: @(YES),
  85. (const NSString *)kCGImageSourceThumbnailMaxPixelSize: @(scaledMaxPixelSize)
  86. };
  87. CGImageRef scaled = CGImageSourceCreateThumbnailAtIndex(imageData, 0, params);
  88. CFRelease(imageData);
  89. if (nil == scaled) {
  90. [[[FBErrorBuilder builder]
  91. withDescriptionFormat:@"Failed to scale the image"]
  92. buildError:error];
  93. return nil;
  94. }
  95. NSData *resData = [self jpegDataWithImage:scaled
  96. compressionQuality:compressionQuality];
  97. if (nil == resData) {
  98. [[[FBErrorBuilder builder]
  99. withDescriptionFormat:@"Failed to compress the image to JPEG format"]
  100. buildError:error];
  101. }
  102. CGImageRelease(scaled);
  103. return resData;
  104. }
  105. - (nullable NSData *)scaledImageWithImage:(NSData *)image
  106. uti:(UTType *)uti
  107. rect:(CGRect)rect
  108. scalingFactor:(CGFloat)scalingFactor
  109. compressionQuality:(CGFloat)compressionQuality
  110. error:(NSError **)error
  111. {
  112. UIImage *uiImage = [UIImage imageWithData:image];
  113. CGSize size = uiImage.size;
  114. CGSize scaledSize = CGSizeMake(size.width * scalingFactor, size.height * scalingFactor);
  115. UIGraphicsBeginImageContext(scaledSize);
  116. UIImageOrientation orientation = uiImage.imageOrientation;
  117. #if !TARGET_OS_TV
  118. if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) {
  119. orientation = UIImageOrientationUp;
  120. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) {
  121. orientation = UIImageOrientationDown;
  122. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) {
  123. orientation = UIImageOrientationRight;
  124. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) {
  125. orientation = UIImageOrientationLeft;
  126. }
  127. #endif
  128. uiImage = [UIImage imageWithCGImage:(CGImageRef)uiImage.CGImage
  129. scale:uiImage.scale
  130. orientation:orientation];
  131. [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
  132. UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
  133. UIGraphicsEndImageContext();
  134. if (!CGRectIsNull(rect)) {
  135. UIGraphicsBeginImageContext(rect.size);
  136. [resultImage drawAtPoint:CGPointMake(-rect.origin.x, -rect.origin.y)];
  137. resultImage = UIGraphicsGetImageFromCurrentImageContext();
  138. UIGraphicsEndImageContext();
  139. }
  140. return [uti conformsToType:UTTypePNG]
  141. ? UIImagePNGRepresentation(resultImage)
  142. : UIImageJPEGRepresentation(resultImage, compressionQuality);
  143. }
  144. - (nullable NSData *)jpegDataWithImage:(CGImageRef)imageRef
  145. compressionQuality:(CGFloat)compressionQuality
  146. {
  147. NSMutableData *newImageData = [NSMutableData data];
  148. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData(
  149. (__bridge CFMutableDataRef) newImageData,
  150. (__bridge CFStringRef) UTTypeJPEG.identifier,
  151. 1,
  152. NULL);
  153. CFDictionaryRef compressionOptions = (__bridge CFDictionaryRef)@{
  154. (const NSString *)kCGImageDestinationLossyCompressionQuality: @(compressionQuality)
  155. };
  156. CGImageDestinationAddImage(imageDestination, imageRef, compressionOptions);
  157. if(!CGImageDestinationFinalize(imageDestination)) {
  158. [FBLogger log:@"Failed to write the image"];
  159. newImageData = nil;
  160. }
  161. CFRelease(imageDestination);
  162. return newImageData;
  163. }
  164. + (CGSize)imageSizeWithImage:(CGImageSourceRef)imageSource
  165. {
  166. NSDictionary *options = @{
  167. (const NSString *)kCGImageSourceShouldCache: @(NO)
  168. };
  169. CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (CFDictionaryRef)options);
  170. NSNumber *width = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelWidth];
  171. NSNumber *height = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelHeight];
  172. CGSize size = CGSizeMake([width floatValue], [height floatValue]);
  173. CFRelease(properties);
  174. return size;
  175. }
  176. @end