PINProgressiveImage.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. //
  2. // PINProgressiveImage.m
  3. // Pods
  4. //
  5. // Created by Garrett Moon on 2/9/15.
  6. //
  7. //
  8. #import "PINProgressiveImage.h"
  9. #import <ImageIO/ImageIO.h>
  10. #import <Accelerate/Accelerate.h>
  11. #import "PINRemoteImage.h"
  12. #import "PINImage+DecodedImage.h"
  13. @interface PINProgressiveImage ()
  14. @property (nonatomic, strong) NSMutableData *mutableData;
  15. @property (nonatomic, assign) int64_t expectedNumberOfBytes;
  16. @property (nonatomic, assign) CGImageSourceRef imageSource;
  17. @property (nonatomic, assign) CGSize size;
  18. @property (nonatomic, assign) BOOL isProgressiveJPEG;
  19. @property (nonatomic, assign) NSUInteger currentThreshold;
  20. @property (nonatomic, assign) float bytesPerSecond;
  21. @property (nonatomic, assign) NSUInteger scannedByte;
  22. @property (nonatomic, assign) NSInteger sosCount;
  23. @property (nonatomic, strong) NSLock *lock;
  24. #if DEBUG
  25. @property (nonatomic, assign) CFTimeInterval scanTime;
  26. #endif
  27. @end
  28. @implementation PINProgressiveImage
  29. @synthesize progressThresholds = _progressThresholds;
  30. @synthesize estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
  31. @synthesize startTime = _startTime;
  32. - (instancetype)init
  33. {
  34. if (self = [super init]) {
  35. self.lock = [[NSLock alloc] init];
  36. self.lock.name = @"PINProgressiveImage";
  37. _imageSource = CGImageSourceCreateIncremental(NULL);;
  38. self.size = CGSizeZero;
  39. self.isProgressiveJPEG = NO;
  40. self.currentThreshold = 0;
  41. self.progressThresholds = @[@0.00, @0.35, @0.65];
  42. self.startTime = CACurrentMediaTime();
  43. self.estimatedRemainingTimeThreshold = -1;
  44. self.sosCount = 0;
  45. self.scannedByte = 0;
  46. #if DEBUG
  47. self.scanTime = 0;
  48. #endif
  49. }
  50. return self;
  51. }
  52. - (void)dealloc
  53. {
  54. [self.lock lock];
  55. if (self.imageSource) {
  56. CFRelease(_imageSource);
  57. }
  58. [self.lock unlock];
  59. }
  60. #pragma mark - public
  61. - (void)setProgressThresholds:(NSArray *)progressThresholds
  62. {
  63. [self.lock lock];
  64. _progressThresholds = [progressThresholds copy];
  65. [self.lock unlock];
  66. }
  67. - (NSArray *)progressThresholds
  68. {
  69. [self.lock lock];
  70. NSArray *progressThresholds = _progressThresholds;
  71. [self.lock unlock];
  72. return progressThresholds;
  73. }
  74. - (void)setEstimatedRemainingTimeThreshold:(CFTimeInterval)estimatedRemainingTimeThreshold
  75. {
  76. [self.lock lock];
  77. _estimatedRemainingTimeThreshold = estimatedRemainingTimeThreshold;
  78. [self.lock unlock];
  79. }
  80. - (CFTimeInterval)estimatedRemainingTimeThreshold
  81. {
  82. [self.lock lock];
  83. CFTimeInterval estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
  84. [self.lock unlock];
  85. return estimatedRemainingTimeThreshold;
  86. }
  87. - (void)setStartTime:(CFTimeInterval)startTime
  88. {
  89. [self.lock lock];
  90. _startTime = startTime;
  91. [self.lock unlock];
  92. }
  93. - (CFTimeInterval)startTime
  94. {
  95. [self.lock lock];
  96. CFTimeInterval startTime = _startTime;
  97. [self.lock unlock];
  98. return startTime;
  99. }
  100. - (void)updateProgressiveImageWithData:(NSData *)data expectedNumberOfBytes:(int64_t)expectedNumberOfBytes
  101. {
  102. [self.lock lock];
  103. if (self.mutableData == nil) {
  104. NSUInteger bytesToAlloc = 0;
  105. if (expectedNumberOfBytes > 0) {
  106. bytesToAlloc = (NSUInteger)expectedNumberOfBytes;
  107. }
  108. self.mutableData = [[NSMutableData alloc] initWithCapacity:bytesToAlloc];
  109. self.expectedNumberOfBytes = expectedNumberOfBytes;
  110. }
  111. [self.mutableData appendData:data];
  112. while ([self hasCompletedFirstScan] == NO && self.scannedByte < self.mutableData.length) {
  113. #if DEBUG
  114. CFTimeInterval start = CACurrentMediaTime();
  115. #endif
  116. NSUInteger startByte = self.scannedByte;
  117. if (startByte > 0) {
  118. startByte--;
  119. }
  120. if ([self scanForSOSinData:self.mutableData startByte:startByte scannedByte:&_scannedByte]) {
  121. self.sosCount++;
  122. }
  123. #if DEBUG
  124. CFTimeInterval total = CACurrentMediaTime() - start;
  125. self.scanTime += total;
  126. #endif
  127. }
  128. if (self.imageSource) {
  129. CGImageSourceUpdateData(self.imageSource, (CFDataRef)self.mutableData, NO);
  130. }
  131. [self.lock unlock];
  132. }
  133. - (PINImage *)currentImageBlurred:(BOOL)blurred maxProgressiveRenderSize:(CGSize)maxProgressiveRenderSize renderedImageQuality:(out CGFloat *)renderedImageQuality
  134. {
  135. [self.lock lock];
  136. if (self.imageSource == nil) {
  137. [self.lock unlock];
  138. return nil;
  139. }
  140. if (self.currentThreshold == _progressThresholds.count) {
  141. [self.lock unlock];
  142. return nil;
  143. }
  144. if (_estimatedRemainingTimeThreshold > 0 && self.estimatedRemainingTime < _estimatedRemainingTimeThreshold) {
  145. [self.lock unlock];
  146. return nil;
  147. }
  148. if ([self hasCompletedFirstScan] == NO) {
  149. [self.lock unlock];
  150. return nil;
  151. }
  152. #if DEBUG
  153. if (self.scanTime > 0) {
  154. PINLog(@"scan time: %f", self.scanTime);
  155. self.scanTime = 0;
  156. }
  157. #endif
  158. PINImage *currentImage = nil;
  159. //Size information comes after JFIF so jpeg properties should be available at or before size?
  160. if (self.size.width <= 0 || self.size.height <= 0) {
  161. //attempt to get size info
  162. NSDictionary *imageProperties = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(self.imageSource, 0, NULL));
  163. CGSize size = self.size;
  164. if (size.width <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelWidth]) {
  165. size.width = [imageProperties[(NSString *)kCGImagePropertyPixelWidth] floatValue];
  166. }
  167. if (size.height <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelHeight]) {
  168. size.height = [imageProperties[(NSString *)kCGImagePropertyPixelHeight] floatValue];
  169. }
  170. self.size = size;
  171. NSDictionary *jpegProperties = imageProperties[(NSString *)kCGImagePropertyJFIFDictionary];
  172. NSNumber *isProgressive = jpegProperties[(NSString *)kCGImagePropertyJFIFIsProgressive];
  173. self.isProgressiveJPEG = jpegProperties && [isProgressive boolValue];
  174. }
  175. if (self.size.width > maxProgressiveRenderSize.width || self.size.height > maxProgressiveRenderSize.height) {
  176. [self.lock unlock];
  177. return nil;
  178. }
  179. float progress = 0;
  180. if (self.expectedNumberOfBytes > 0) {
  181. progress = (float)self.mutableData.length / (float)self.expectedNumberOfBytes;
  182. }
  183. //Don't bother if we're basically done
  184. if (progress >= 0.99) {
  185. [self.lock unlock];
  186. return nil;
  187. }
  188. if (self.isProgressiveJPEG && self.size.width > 0 && self.size.height > 0 && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
  189. while (self.currentThreshold < _progressThresholds.count && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
  190. self.currentThreshold++;
  191. }
  192. PINLog(@"Generating preview image");
  193. CGImageRef image = CGImageSourceCreateImageAtIndex(self.imageSource, 0, NULL);
  194. if (image) {
  195. if (blurred) {
  196. currentImage = [self postProcessImage:[PINImage imageWithCGImage:image] withProgress:progress];
  197. } else {
  198. currentImage = [PINImage imageWithCGImage:image];
  199. }
  200. CGImageRelease(image);
  201. if (renderedImageQuality) {
  202. *renderedImageQuality = progress;
  203. }
  204. }
  205. }
  206. [self.lock unlock];
  207. return currentImage;
  208. }
  209. - (NSData *)data
  210. {
  211. [self.lock lock];
  212. NSData *data = [self.mutableData copy];
  213. [self.lock unlock];
  214. return data;
  215. }
  216. #pragma mark - private
  217. //Must be called within lock
  218. - (BOOL)scanForSOSinData:(NSData *)data startByte:(NSUInteger)startByte scannedByte:(NSUInteger *)scannedByte
  219. {
  220. //check if we have a complete scan
  221. Byte scanMarker[2];
  222. //SOS marker
  223. scanMarker[0] = 0xFF;
  224. scanMarker[1] = 0xDA;
  225. //scan one byte back in case we only got half the SOS on the last data append
  226. NSRange scanRange;
  227. scanRange.location = startByte;
  228. scanRange.length = data.length - scanRange.location;
  229. NSRange sosRange = [data rangeOfData:[NSData dataWithBytes:scanMarker length:2] options:0 range:scanRange];
  230. if (sosRange.location != NSNotFound) {
  231. if (scannedByte) {
  232. *scannedByte = NSMaxRange(sosRange);
  233. }
  234. return YES;
  235. }
  236. if (scannedByte) {
  237. *scannedByte = NSMaxRange(scanRange);
  238. }
  239. return NO;
  240. }
  241. //Must be called within lock
  242. - (BOOL)hasCompletedFirstScan
  243. {
  244. return self.sosCount >= 2;
  245. }
  246. //Must be called within lock
  247. - (float)bytesPerSecond
  248. {
  249. CFTimeInterval length = CACurrentMediaTime() - _startTime;
  250. return self.mutableData.length / length;
  251. }
  252. //Must be called within lock
  253. - (CFTimeInterval)estimatedRemainingTime
  254. {
  255. if (self.expectedNumberOfBytes < 0) {
  256. return MAXFLOAT;
  257. }
  258. NSUInteger remainingBytes = (NSUInteger)self.expectedNumberOfBytes - self.mutableData.length;
  259. if (remainingBytes == 0) {
  260. return 0;
  261. }
  262. float bytesPerSecond = self.bytesPerSecond;
  263. if (bytesPerSecond == 0) {
  264. return MAXFLOAT;
  265. }
  266. return remainingBytes / self.bytesPerSecond;
  267. }
  268. //Must be called within lock
  269. //Heavily cribbed from https://developer.apple.com/library/ios/samplecode/UIImageEffects/Listings/UIImageEffects_UIImageEffects_m.html#//apple_ref/doc/uid/DTS40013396-UIImageEffects_UIImageEffects_m-DontLinkElementID_9
  270. - (PINImage *)postProcessImage:(PINImage *)inputImage withProgress:(float)progress
  271. {
  272. PINImage *outputImage = nil;
  273. CGImageRef inputImageRef = CGImageRetain(inputImage.CGImage);
  274. if (inputImageRef == nil) {
  275. return nil;
  276. }
  277. CGSize inputSize = inputImage.size;
  278. if (inputSize.width < 1 ||
  279. inputSize.height < 1) {
  280. CGImageRelease(inputImageRef);
  281. return nil;
  282. }
  283. #if PIN_TARGET_IOS
  284. CGFloat imageScale = inputImage.scale;
  285. #elif PIN_TARGET_MAC
  286. // TODO: What scale factor should be used here?
  287. CGFloat imageScale = [[NSScreen mainScreen] backingScaleFactor];
  288. #endif
  289. CGFloat radius = (inputImage.size.width / 25.0) * MAX(0, 1.0 - progress);
  290. radius *= imageScale;
  291. //we'll round the radius to a whole number below anyway,
  292. if (radius < FLT_EPSILON) {
  293. CGImageRelease(inputImageRef);
  294. return inputImage;
  295. }
  296. CGContextRef ctx;
  297. #if PIN_TARGET_IOS
  298. UIGraphicsBeginImageContextWithOptions(inputSize, YES, imageScale);
  299. ctx = UIGraphicsGetCurrentContext();
  300. #elif PIN_TARGET_MAC
  301. ctx = CGBitmapContextCreate(0, inputSize.width, inputSize.height, 8, 0, [NSColorSpace genericRGBColorSpace].CGColorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
  302. #endif
  303. if (ctx) {
  304. #if PIN_TARGET_IOS
  305. CGContextScaleCTM(ctx, 1.0, -1.0);
  306. CGContextTranslateCTM(ctx, 0, -inputSize.height);
  307. #endif
  308. vImage_Buffer effectInBuffer;
  309. vImage_Buffer scratchBuffer;
  310. vImage_Buffer *inputBuffer;
  311. vImage_Buffer *outputBuffer;
  312. vImage_CGImageFormat format = {
  313. .bitsPerComponent = 8,
  314. .bitsPerPixel = 32,
  315. .colorSpace = NULL,
  316. // (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little)
  317. // requests a BGRA buffer.
  318. .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
  319. .version = 0,
  320. .decode = NULL,
  321. .renderingIntent = kCGRenderingIntentDefault
  322. };
  323. vImage_Error e = vImageBuffer_InitWithCGImage(&effectInBuffer, &format, NULL, inputImage.CGImage, kvImagePrintDiagnosticsToConsole);
  324. if (e == kvImageNoError)
  325. {
  326. e = vImageBuffer_Init(&scratchBuffer, effectInBuffer.height, effectInBuffer.width, format.bitsPerPixel, kvImageNoFlags);
  327. if (e == kvImageNoError) {
  328. inputBuffer = &effectInBuffer;
  329. outputBuffer = &scratchBuffer;
  330. // A description of how to compute the box kernel width from the Gaussian
  331. // radius (aka standard deviation) appears in the SVG spec:
  332. // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
  333. //
  334. // For larger values of 's' (s >= 2.0), an approximation can be used: Three
  335. // successive box-blurs build a piece-wise quadratic convolution kernel, which
  336. // approximates the Gaussian kernel to within roughly 3%.
  337. //
  338. // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
  339. //
  340. // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
  341. //
  342. if (radius - 2. < __FLT_EPSILON__)
  343. radius = 2.;
  344. uint32_t wholeRadius = floor((radius * 3. * sqrt(2 * M_PI) / 4 + 0.5) / 2);
  345. wholeRadius |= 1; // force wholeRadius to be odd so that the three box-blur methodology works.
  346. //calculate the size necessary for vImageBoxConvolve_ARGB8888, this does not actually do any operations.
  347. NSInteger tempBufferSize = vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, NULL, 0, 0, wholeRadius, wholeRadius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
  348. void *tempBuffer = malloc(tempBufferSize);
  349. if (tempBuffer) {
  350. //errors can be ignored because we've passed in allocated memory
  351. vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
  352. vImageBoxConvolve_ARGB8888(outputBuffer, inputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
  353. vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
  354. free(tempBuffer);
  355. //switch input and output
  356. vImage_Buffer *temp = inputBuffer;
  357. inputBuffer = outputBuffer;
  358. outputBuffer = temp;
  359. CGImageRef effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, &cleanupBuffer, NULL, kvImageNoAllocate, NULL);
  360. if (effectCGImage == NULL) {
  361. //if creating the cgimage failed, the cleanup buffer on input buffer will not be called, we must dealloc ourselves
  362. free(inputBuffer->data);
  363. } else {
  364. // draw effect image
  365. CGContextSaveGState(ctx);
  366. CGContextDrawImage(ctx, CGRectMake(0, 0, inputSize.width, inputSize.height), effectCGImage);
  367. CGContextRestoreGState(ctx);
  368. CGImageRelease(effectCGImage);
  369. }
  370. // Cleanup
  371. free(outputBuffer->data);
  372. #if PIN_TARGET_IOS
  373. outputImage = UIGraphicsGetImageFromCurrentImageContext();
  374. #elif PIN_TARGET_MAC
  375. CGImageRef outputImageRef = CGBitmapContextCreateImage(ctx);
  376. outputImage = [[NSImage alloc] initWithCGImage:outputImageRef size:inputSize];
  377. CFRelease(outputImageRef);
  378. #endif
  379. }
  380. } else {
  381. if (scratchBuffer.data) {
  382. free(scratchBuffer.data);
  383. }
  384. free(effectInBuffer.data);
  385. }
  386. } else {
  387. if (effectInBuffer.data) {
  388. free(effectInBuffer.data);
  389. }
  390. }
  391. }
  392. #if PIN_TARGET_IOS
  393. UIGraphicsEndImageContext();
  394. #endif
  395. CGImageRelease(inputImageRef);
  396. return outputImage;
  397. }
  398. // Helper function to handle deferred cleanup of a buffer.
  399. static void cleanupBuffer(void *userData, void *buf_data)
  400. {
  401. free(buf_data);
  402. }
  403. @end