mirror of
https://github.com/zhigang1992/PINRemoteImage.git
synced 2026-06-10 07:10:08 +08:00
396 lines
12 KiB
Objective-C
396 lines
12 KiB
Objective-C
//
|
|
// PINProgressiveImage.m
|
|
// Pods
|
|
//
|
|
// Created by Garrett Moon on 2/9/15.
|
|
//
|
|
//
|
|
|
|
#import "PINProgressiveImage.h"
|
|
|
|
#import <ImageIO/ImageIO.h>
|
|
#import <CoreImage/CoreImage.h>
|
|
|
|
#import "PINRemoteImage.h"
|
|
#import "UIImage+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, strong) UIImage *cachedImage;
|
|
@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
|
|
|
|
@property (nonatomic, assign) BOOL inBackground;
|
|
|
|
@end
|
|
|
|
@implementation PINProgressiveImage
|
|
|
|
@synthesize progressThresholds = _progressThresholds;
|
|
@synthesize estimatedRemainingTimeThreshold = _estimatedRemainingTimeThreshold;
|
|
@synthesize startTime = _startTime;
|
|
|
|
static CIContext *GPUProcessingContext = nil;
|
|
static CIContext *CPUProcessingContext = nil;
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (self = [super init]) {
|
|
self.lock = [[NSLock alloc] init];
|
|
self.lock.name = @"PINProgressiveImage";
|
|
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
//CIContexts are immutable and threadsafe
|
|
CPUProcessingContext = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @YES}];
|
|
GPUProcessingContext = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @NO, kCIContextPriorityRequestLow: @YES}];
|
|
});
|
|
|
|
_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
|
|
|
|
#if PIN_APP_EXTENSIONS
|
|
self.inBackground = YES;
|
|
#else
|
|
self.inBackground = [[UIApplication sharedApplication] applicationState] != UIApplicationStateActive;
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self.lock lock];
|
|
if (self.imageSource) {
|
|
CFRelease(_imageSource);
|
|
}
|
|
[self.lock unlock];
|
|
#if !PIN_APP_EXTENSIONS
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
#endif
|
|
}
|
|
|
|
#pragma mark - NSNotifications
|
|
|
|
- (void)willResignActive:(NSNotification *)notification
|
|
{
|
|
[self.lock lock];
|
|
self.inBackground = YES;
|
|
[self.lock unlock];
|
|
}
|
|
|
|
- (void)willEnterForeground:(NSNotification *)notification
|
|
{
|
|
[self.lock lock];
|
|
self.inBackground = NO;
|
|
[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];
|
|
}
|
|
|
|
- (UIImage *)currentImage
|
|
{
|
|
[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
|
|
|
|
UIImage *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];
|
|
}
|
|
|
|
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) {
|
|
currentImage = [self postProcessImage:[UIImage imageWithCGImage:image] withProgress:progress];
|
|
CGImageRelease(image);
|
|
}
|
|
}
|
|
|
|
[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
|
|
- (UIImage *)postProcessImage:(UIImage *)inputImage withProgress:(float)progress
|
|
{
|
|
CGImageRef inputImageRef = CGImageRetain(inputImage.CGImage);
|
|
if (inputImageRef == nil) {
|
|
return nil;
|
|
}
|
|
|
|
CIFilter *gaussianFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
|
|
[gaussianFilter setDefaults];
|
|
|
|
UIImage *outputUIImage = nil;
|
|
|
|
CIContext *processingContext = GPUProcessingContext;
|
|
if (self.inBackground) {
|
|
processingContext = CPUProcessingContext;
|
|
}
|
|
|
|
CGSize maxInput = processingContext.inputImageMaximumSize;
|
|
CGSize maxOutput = processingContext.outputImageMaximumSize;
|
|
CGSize inputSize = inputImage.size;
|
|
if (maxInput.height < inputSize.height ||
|
|
maxInput.width < inputSize.width ||
|
|
maxOutput.height < inputSize.height ||
|
|
maxOutput.width < inputSize.width) {
|
|
CGImageRelease(inputImageRef);
|
|
return nil;
|
|
}
|
|
|
|
if (processingContext && gaussianFilter) {
|
|
[gaussianFilter setValue:[CIImage imageWithCGImage:inputImageRef]
|
|
forKey:kCIInputImageKey];
|
|
|
|
NSNumber *radius = @((inputImage.size.width / 50.0) * MAX(0, 1.0 - progress));
|
|
[gaussianFilter setValue:radius
|
|
forKey:kCIInputRadiusKey];
|
|
|
|
CIImage *outputImage = [gaussianFilter outputImage];
|
|
if (outputImage) {
|
|
CGImageRef outputImageRef = [processingContext createCGImage:outputImage fromRect:CGRectMake(0, 0, inputImage.size.width, inputImage.size.height)];
|
|
|
|
if (outputImageRef) {
|
|
outputUIImage = [UIImage imageWithCGImage:outputImageRef];
|
|
CGImageRelease(outputImageRef);
|
|
}
|
|
}
|
|
}
|
|
|
|
CGImageRelease(inputImageRef);
|
|
|
|
return outputUIImage;
|
|
}
|
|
|
|
@end
|