FBImageProcessor.m 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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 "FBImageProcessor.h"
  10. #import <ImageIO/ImageIO.h>
  11. #import <UIKit/UIKit.h>
  12. @import UniformTypeIdentifiers;
  13. #import "FBConfiguration.h"
  14. #import "FBErrorBuilder.h"
  15. #import "FBImageUtils.h"
  16. #import "FBLogger.h"
  17. const CGFloat FBMinScalingFactor = 0.01f;
  18. const CGFloat FBMaxScalingFactor = 1.0f;
  19. const CGFloat FBMinCompressionQuality = 0.0f;
  20. const CGFloat FBMaxCompressionQuality = 1.0f;
  21. @interface FBImageProcessor ()
  22. @property (nonatomic) NSData *nextImage;
  23. @property (nonatomic, readonly) NSLock *nextImageLock;
  24. @property (nonatomic, readonly) dispatch_queue_t scalingQueue;
  25. @end
  26. @implementation FBImageProcessor
  27. - (id)init
  28. {
  29. self = [super init];
  30. if (self) {
  31. _nextImageLock = [[NSLock alloc] init];
  32. _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL);
  33. }
  34. return self;
  35. }
  36. - (void)submitImageData:(NSData *)image
  37. scalingFactor:(CGFloat)scalingFactor
  38. completionHandler:(void (^)(NSData *))completionHandler
  39. {
  40. [self.nextImageLock lock];
  41. if (self.nextImage != nil) {
  42. [FBLogger verboseLog:@"Discarding screenshot"];
  43. }
  44. self.nextImage = image;
  45. [self.nextImageLock unlock];
  46. #pragma clang diagnostic push
  47. #pragma clang diagnostic ignored "-Wcompletion-handler"
  48. dispatch_async(self.scalingQueue, ^{
  49. [self.nextImageLock lock];
  50. NSData *nextImageData = self.nextImage;
  51. self.nextImage = nil;
  52. [self.nextImageLock unlock];
  53. if (nextImageData == nil) {
  54. return;
  55. }
  56. // We do not want this value to be too high because then we get images larger in size than original ones
  57. // Although, we also don't want to lose too much of the quality on recompression
  58. CGFloat recompressionQuality = MAX(0.9,
  59. MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
  60. NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData
  61. scalingFactor:scalingFactor
  62. uti:UTTypeJPEG
  63. compressionQuality:recompressionQuality
  64. // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata
  65. // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812
  66. fixOrientation:FBConfiguration.mjpegShouldFixOrientation
  67. desiredOrientation:nil];
  68. completionHandler(thumbnailData ?: nextImageData);
  69. });
  70. #pragma clang diagnostic pop
  71. }
  72. + (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData
  73. scalingFactor:(CGFloat)scalingFactor
  74. uti:(UTType *)uti
  75. compressionQuality:(CGFloat)compressionQuality
  76. fixOrientation:(BOOL)fixOrientation
  77. desiredOrientation:(nullable NSNumber *)orientation
  78. {
  79. scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor));
  80. BOOL usesScaling = scalingFactor > 0.0 && scalingFactor < FBMaxScalingFactor;
  81. @autoreleasepool {
  82. if (!usesScaling && !fixOrientation) {
  83. return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
  84. }
  85. UIImage *image = [UIImage imageWithData:imageData];
  86. if (nil == image
  87. || ((image.imageOrientation == UIImageOrientationUp || !fixOrientation) && !usesScaling)) {
  88. return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
  89. }
  90. CGSize scaledSize = CGSizeMake(image.size.width * scalingFactor, image.size.height * scalingFactor);
  91. if (!fixOrientation && usesScaling) {
  92. dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  93. __block UIImage *result = nil;
  94. [image prepareThumbnailOfSize:scaledSize
  95. completionHandler:^(UIImage * _Nullable thumbnail) {
  96. result = thumbnail;
  97. dispatch_semaphore_signal(semaphore);
  98. }];
  99. dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  100. if (nil == result) {
  101. return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
  102. }
  103. return [uti conformsToType:UTTypePNG]
  104. ? UIImagePNGRepresentation(result)
  105. : UIImageJPEGRepresentation(result, compressionQuality);
  106. }
  107. UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
  108. format.scale = scalingFactor;
  109. UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize
  110. format:format];
  111. UIImageOrientation desiredOrientation = orientation == nil
  112. ? image.imageOrientation
  113. : (UIImageOrientation)orientation.integerValue;
  114. UIImage *uiImage = [UIImage imageWithCGImage:(CGImageRef)image.CGImage
  115. scale:image.scale
  116. orientation:desiredOrientation];
  117. return [uti conformsToType:UTTypePNG]
  118. ? [renderer PNGDataWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
  119. [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
  120. }]
  121. : [renderer JPEGDataWithCompressionQuality:compressionQuality
  122. actions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
  123. [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
  124. }];
  125. }
  126. }
  127. - (nullable NSData *)scaledImageWithData:(NSData *)imageData
  128. uti:(UTType *)uti
  129. scalingFactor:(CGFloat)scalingFactor
  130. compressionQuality:(CGFloat)compressionQuality
  131. error:(NSError **)error
  132. {
  133. NSNumber *orientation = nil;
  134. #if !TARGET_OS_TV
  135. if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) {
  136. orientation = @(UIImageOrientationUp);
  137. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) {
  138. orientation = @(UIImageOrientationDown);
  139. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) {
  140. orientation = @(UIImageOrientationRight);
  141. } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) {
  142. orientation = @(UIImageOrientationLeft);
  143. }
  144. #endif
  145. NSData *resultData = [self.class fixedImageDataWithImageData:imageData
  146. scalingFactor:scalingFactor
  147. uti:uti
  148. compressionQuality:compressionQuality
  149. fixOrientation:YES
  150. desiredOrientation:orientation];
  151. return resultData ?: imageData;
  152. }
  153. @end