| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- //
- // PINProgressiveImage.m
- // Pods
- //
- // Created by Garrett Moon on 2/9/15.
- //
- //
- #import "PINProgressiveImage.h"
- #import <ImageIO/ImageIO.h>
- #import <Accelerate/Accelerate.h>
- #import "PINRemoteImage.h"
- #import "PINImage+DecodedImage.h"
- @interface PINProgressiveImage ()
- @property (nonatomic, strong) NSMutableData *mutableData;
- @property (nonatomic, assign) int64_t expectedNumberOfBytes;
- @property (nonatomic, assign) CGImageSourceRef imageSource;
- @property (nonatomic, assign) CGSize size;
- @property (nonatomic, assign) BOOL isProgressiveJPEG;
- @property (nonatomic, assign) NSUInteger currentThreshold;
- @property (nonatomic, assign) float bytesPerSecond;
- @property (nonatomic, assign) NSUInteger scannedByte;
- @property (nonatomic, assign) NSInteger sosCount;
- @property (nonatomic, strong) NSLock *lock;
- #if DEBUG
- @property (nonatomic, assign) CFTimeInterval scanTime;
- #endif
- @end
- @implementation PINProgressiveImage
- @synthesize progressThresholds = _progressThresholds;
- @synthesize estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
- @synthesize startTime = _startTime;
- - (instancetype)init
- {
- if (self = [super init]) {
- self.lock = [[NSLock alloc] init];
- self.lock.name = @"PINProgressiveImage";
-
- _imageSource = CGImageSourceCreateIncremental(NULL);;
- self.size = CGSizeZero;
- self.isProgressiveJPEG = NO;
- self.currentThreshold = 0;
- self.progressThresholds = @[@0.00, @0.35, @0.65];
- self.startTime = CACurrentMediaTime();
- self.estimatedRemainingTimeThreshold = -1;
- self.sosCount = 0;
- self.scannedByte = 0;
- #if DEBUG
- self.scanTime = 0;
- #endif
- }
- return self;
- }
- - (void)dealloc
- {
- [self.lock lock];
- if (self.imageSource) {
- CFRelease(_imageSource);
- }
- [self.lock unlock];
- }
- #pragma mark - public
- - (void)setProgressThresholds:(NSArray *)progressThresholds
- {
- [self.lock lock];
- _progressThresholds = [progressThresholds copy];
- [self.lock unlock];
- }
- - (NSArray *)progressThresholds
- {
- [self.lock lock];
- NSArray *progressThresholds = _progressThresholds;
- [self.lock unlock];
- return progressThresholds;
- }
- - (void)setEstimatedRemainingTimeThreshold:(CFTimeInterval)estimatedRemainingTimeThreshold
- {
- [self.lock lock];
- _estimatedRemainingTimeThreshold = estimatedRemainingTimeThreshold;
- [self.lock unlock];
- }
- - (CFTimeInterval)estimatedRemainingTimeThreshold
- {
- [self.lock lock];
- CFTimeInterval estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
- [self.lock unlock];
- return estimatedRemainingTimeThreshold;
- }
- - (void)setStartTime:(CFTimeInterval)startTime
- {
- [self.lock lock];
- _startTime = startTime;
- [self.lock unlock];
- }
- - (CFTimeInterval)startTime
- {
- [self.lock lock];
- CFTimeInterval startTime = _startTime;
- [self.lock unlock];
- return startTime;
- }
- - (void)updateProgressiveImageWithData:(NSData *)data expectedNumberOfBytes:(int64_t)expectedNumberOfBytes
- {
- [self.lock lock];
- if (self.mutableData == nil) {
- NSUInteger bytesToAlloc = 0;
- if (expectedNumberOfBytes > 0) {
- bytesToAlloc = (NSUInteger)expectedNumberOfBytes;
- }
- self.mutableData = [[NSMutableData alloc] initWithCapacity:bytesToAlloc];
- self.expectedNumberOfBytes = expectedNumberOfBytes;
- }
- [self.mutableData appendData:data];
-
- while ([self hasCompletedFirstScan] == NO && self.scannedByte < self.mutableData.length) {
- #if DEBUG
- CFTimeInterval start = CACurrentMediaTime();
- #endif
- NSUInteger startByte = self.scannedByte;
- if (startByte > 0) {
- startByte--;
- }
- if ([self scanForSOSinData:self.mutableData startByte:startByte scannedByte:&_scannedByte]) {
- self.sosCount++;
- }
- #if DEBUG
- CFTimeInterval total = CACurrentMediaTime() - start;
- self.scanTime += total;
- #endif
- }
-
- if (self.imageSource) {
- CGImageSourceUpdateData(self.imageSource, (CFDataRef)self.mutableData, NO);
- }
- [self.lock unlock];
- }
- - (PINImage *)currentImageBlurred:(BOOL)blurred maxProgressiveRenderSize:(CGSize)maxProgressiveRenderSize renderedImageQuality:(out CGFloat *)renderedImageQuality
- {
- [self.lock lock];
- if (self.imageSource == nil) {
- [self.lock unlock];
- return nil;
- }
-
- if (self.currentThreshold == _progressThresholds.count) {
- [self.lock unlock];
- return nil;
- }
-
- if (_estimatedRemainingTimeThreshold > 0 && self.estimatedRemainingTime < _estimatedRemainingTimeThreshold) {
- [self.lock unlock];
- return nil;
- }
-
- if ([self hasCompletedFirstScan] == NO) {
- [self.lock unlock];
- return nil;
- }
-
- #if DEBUG
- if (self.scanTime > 0) {
- PINLog(@"scan time: %f", self.scanTime);
- self.scanTime = 0;
- }
- #endif
-
- PINImage *currentImage = nil;
-
- //Size information comes after JFIF so jpeg properties should be available at or before size?
- if (self.size.width <= 0 || self.size.height <= 0) {
- //attempt to get size info
- NSDictionary *imageProperties = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(self.imageSource, 0, NULL));
- CGSize size = self.size;
- if (size.width <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelWidth]) {
- size.width = [imageProperties[(NSString *)kCGImagePropertyPixelWidth] floatValue];
- }
-
- if (size.height <= 0 && imageProperties[(NSString *)kCGImagePropertyPixelHeight]) {
- size.height = [imageProperties[(NSString *)kCGImagePropertyPixelHeight] floatValue];
- }
-
- self.size = size;
-
- NSDictionary *jpegProperties = imageProperties[(NSString *)kCGImagePropertyJFIFDictionary];
- NSNumber *isProgressive = jpegProperties[(NSString *)kCGImagePropertyJFIFIsProgressive];
- self.isProgressiveJPEG = jpegProperties && [isProgressive boolValue];
- }
-
- if (self.size.width > maxProgressiveRenderSize.width || self.size.height > maxProgressiveRenderSize.height) {
- [self.lock unlock];
- return nil;
- }
-
- float progress = 0;
- if (self.expectedNumberOfBytes > 0) {
- progress = (float)self.mutableData.length / (float)self.expectedNumberOfBytes;
- }
-
- //Don't bother if we're basically done
- if (progress >= 0.99) {
- [self.lock unlock];
- return nil;
- }
-
- if (self.isProgressiveJPEG && self.size.width > 0 && self.size.height > 0 && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
- while (self.currentThreshold < _progressThresholds.count && progress > [_progressThresholds[self.currentThreshold] floatValue]) {
- self.currentThreshold++;
- }
- PINLog(@"Generating preview image");
- CGImageRef image = CGImageSourceCreateImageAtIndex(self.imageSource, 0, NULL);
- if (image) {
- if (blurred) {
- currentImage = [self postProcessImage:[PINImage imageWithCGImage:image] withProgress:progress];
- } else {
- currentImage = [PINImage imageWithCGImage:image];
- }
- CGImageRelease(image);
- if (renderedImageQuality) {
- *renderedImageQuality = progress;
- }
- }
- }
-
- [self.lock unlock];
- return currentImage;
- }
- - (NSData *)data
- {
- [self.lock lock];
- NSData *data = [self.mutableData copy];
- [self.lock unlock];
- return data;
- }
- #pragma mark - private
- //Must be called within lock
- - (BOOL)scanForSOSinData:(NSData *)data startByte:(NSUInteger)startByte scannedByte:(NSUInteger *)scannedByte
- {
- //check if we have a complete scan
- Byte scanMarker[2];
- //SOS marker
- scanMarker[0] = 0xFF;
- scanMarker[1] = 0xDA;
-
- //scan one byte back in case we only got half the SOS on the last data append
- NSRange scanRange;
- scanRange.location = startByte;
- scanRange.length = data.length - scanRange.location;
- NSRange sosRange = [data rangeOfData:[NSData dataWithBytes:scanMarker length:2] options:0 range:scanRange];
- if (sosRange.location != NSNotFound) {
- if (scannedByte) {
- *scannedByte = NSMaxRange(sosRange);
- }
- return YES;
- }
- if (scannedByte) {
- *scannedByte = NSMaxRange(scanRange);
- }
- return NO;
- }
- //Must be called within lock
- - (BOOL)hasCompletedFirstScan
- {
- return self.sosCount >= 2;
- }
- //Must be called within lock
- - (float)bytesPerSecond
- {
- CFTimeInterval length = CACurrentMediaTime() - _startTime;
- return self.mutableData.length / length;
- }
- //Must be called within lock
- - (CFTimeInterval)estimatedRemainingTime
- {
- if (self.expectedNumberOfBytes < 0) {
- return MAXFLOAT;
- }
-
- NSUInteger remainingBytes = (NSUInteger)self.expectedNumberOfBytes - self.mutableData.length;
- if (remainingBytes == 0) {
- return 0;
- }
-
- float bytesPerSecond = self.bytesPerSecond;
- if (bytesPerSecond == 0) {
- return MAXFLOAT;
- }
- return remainingBytes / self.bytesPerSecond;
- }
- //Must be called within lock
- //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
- - (PINImage *)postProcessImage:(PINImage *)inputImage withProgress:(float)progress
- {
- PINImage *outputImage = nil;
- CGImageRef inputImageRef = CGImageRetain(inputImage.CGImage);
- if (inputImageRef == nil) {
- return nil;
- }
-
- CGSize inputSize = inputImage.size;
- if (inputSize.width < 1 ||
- inputSize.height < 1) {
- CGImageRelease(inputImageRef);
- return nil;
- }
- #if PIN_TARGET_IOS
- CGFloat imageScale = inputImage.scale;
- #elif PIN_TARGET_MAC
- // TODO: What scale factor should be used here?
- CGFloat imageScale = [[NSScreen mainScreen] backingScaleFactor];
- #endif
-
- CGFloat radius = (inputImage.size.width / 25.0) * MAX(0, 1.0 - progress);
- radius *= imageScale;
-
- //we'll round the radius to a whole number below anyway,
- if (radius < FLT_EPSILON) {
- CGImageRelease(inputImageRef);
- return inputImage;
- }
-
- CGContextRef ctx;
- #if PIN_TARGET_IOS
- UIGraphicsBeginImageContextWithOptions(inputSize, YES, imageScale);
- ctx = UIGraphicsGetCurrentContext();
- #elif PIN_TARGET_MAC
- ctx = CGBitmapContextCreate(0, inputSize.width, inputSize.height, 8, 0, [NSColorSpace genericRGBColorSpace].CGColorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
- #endif
-
- if (ctx) {
- #if PIN_TARGET_IOS
- CGContextScaleCTM(ctx, 1.0, -1.0);
- CGContextTranslateCTM(ctx, 0, -inputSize.height);
- #endif
-
- vImage_Buffer effectInBuffer;
- vImage_Buffer scratchBuffer;
-
- vImage_Buffer *inputBuffer;
- vImage_Buffer *outputBuffer;
-
- vImage_CGImageFormat format = {
- .bitsPerComponent = 8,
- .bitsPerPixel = 32,
- .colorSpace = NULL,
- // (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little)
- // requests a BGRA buffer.
- .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little,
- .version = 0,
- .decode = NULL,
- .renderingIntent = kCGRenderingIntentDefault
- };
-
- vImage_Error e = vImageBuffer_InitWithCGImage(&effectInBuffer, &format, NULL, inputImage.CGImage, kvImagePrintDiagnosticsToConsole);
- if (e == kvImageNoError)
- {
- e = vImageBuffer_Init(&scratchBuffer, effectInBuffer.height, effectInBuffer.width, format.bitsPerPixel, kvImageNoFlags);
- if (e == kvImageNoError) {
- inputBuffer = &effectInBuffer;
- outputBuffer = &scratchBuffer;
-
- // A description of how to compute the box kernel width from the Gaussian
- // radius (aka standard deviation) appears in the SVG spec:
- // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
- //
- // For larger values of 's' (s >= 2.0), an approximation can be used: Three
- // successive box-blurs build a piece-wise quadratic convolution kernel, which
- // approximates the Gaussian kernel to within roughly 3%.
- //
- // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
- //
- // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
- //
- if (radius - 2. < __FLT_EPSILON__)
- radius = 2.;
- uint32_t wholeRadius = floor((radius * 3. * sqrt(2 * M_PI) / 4 + 0.5) / 2);
-
- wholeRadius |= 1; // force wholeRadius to be odd so that the three box-blur methodology works.
-
- //calculate the size necessary for vImageBoxConvolve_ARGB8888, this does not actually do any operations.
- NSInteger tempBufferSize = vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, NULL, 0, 0, wholeRadius, wholeRadius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
- void *tempBuffer = malloc(tempBufferSize);
-
- if (tempBuffer) {
- //errors can be ignored because we've passed in allocated memory
- vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
- vImageBoxConvolve_ARGB8888(outputBuffer, inputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
- vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, wholeRadius, wholeRadius, NULL, kvImageEdgeExtend);
-
- free(tempBuffer);
-
- //switch input and output
- vImage_Buffer *temp = inputBuffer;
- inputBuffer = outputBuffer;
- outputBuffer = temp;
-
- CGImageRef effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, &cleanupBuffer, NULL, kvImageNoAllocate, NULL);
- if (effectCGImage == NULL) {
- //if creating the cgimage failed, the cleanup buffer on input buffer will not be called, we must dealloc ourselves
- free(inputBuffer->data);
- } else {
- // draw effect image
- CGContextSaveGState(ctx);
- CGContextDrawImage(ctx, CGRectMake(0, 0, inputSize.width, inputSize.height), effectCGImage);
- CGContextRestoreGState(ctx);
- CGImageRelease(effectCGImage);
- }
-
- // Cleanup
- free(outputBuffer->data);
- #if PIN_TARGET_IOS
- outputImage = UIGraphicsGetImageFromCurrentImageContext();
- #elif PIN_TARGET_MAC
- CGImageRef outputImageRef = CGBitmapContextCreateImage(ctx);
- outputImage = [[NSImage alloc] initWithCGImage:outputImageRef size:inputSize];
- CFRelease(outputImageRef);
- #endif
-
- }
- } else {
- if (scratchBuffer.data) {
- free(scratchBuffer.data);
- }
- free(effectInBuffer.data);
- }
- } else {
- if (effectInBuffer.data) {
- free(effectInBuffer.data);
- }
- }
- }
-
- #if PIN_TARGET_IOS
- UIGraphicsEndImageContext();
- #endif
- CGImageRelease(inputImageRef);
-
- return outputImage;
- }
- // Helper function to handle deferred cleanup of a buffer.
- static void cleanupBuffer(void *userData, void *buf_data)
- {
- free(buf_data);
- }
- @end
|