ImageSpeedComparison/ViewController.m (406 lines of code) (raw):
//
// ViewController.m
// ImageSpeedComparison
//
// Created on 9/4/15.
// Copyright (c) 2015 Twitter. All rights reserved.
//
#import <Accelerate/Accelerate.h>
#import <TwitterImagePipeline/TwitterImagePipeline.h>
#import "TIPTestImageFetchDownloadInternalWithStubbing.h"
#import "ViewController.h"
typedef struct {
__unsafe_unretained NSString *type;
const char *name;
const char *file; // provide a URL to load from the inet instead of simulating the load
BOOL isProgressive;
BOOL isAnimated;
} ImageTypeStruct;
static const ImageTypeStruct sImageTypes[] = {
{ @"public.jpeg", "JPEG", "twitterfied.jpg", NO, NO },
{ @"public.jpeg", "PJPEG", "twitterfied.pjpg", YES, NO },
{ @"public.jpeg-2000", "JPEG-2000", "twitterfied.jp2", NO, NO },
{ @"public.png", "PNG", "twitterfied.png", NO, NO },
{ @"public.tiff", "TIFF", "twitterfied.tiff", NO, NO },
{ @"com.compuserve.gif", "GIF", "fireworks_original.gif", NO, YES },
{ @"org.webmproject.webp", "WEBP", "twitterfied.webp", NO, NO },
//{ @"org.webmproject.webp", "Ani-WEBP", "fireworks_original.webp", NO, YES },
{ @"org.webmproject.webp", "Ani-WEBP", "tenor_test.webp", NO, YES },
// { @"com.compuserve.gif", "Static GIF", "https://media3.giphy.com/media/d3F2Dj8zECyDLFpm/v1.Y2lkPWU4MjZjOWZjOGViZWNhZmJmMjk0NDIyZGQzZjM2ZjhkMzhlNGRhZTk5OTYzZjliMQ/200_s.gif", NO, NO },
{ @"public.heic", "HEIC", "twitterfied.heic", NO, NO },
// { @"public.heic", "Ani-HEIC", "starfield_animation.heic", NO, YES },
{ @"public.jpeg", "Small-PJPEG", "twitterfied.small.pjpg", YES, NO },
};
static const NSUInteger kBitrateDribble = 4 * 1000;
static const NSUInteger kBitrate80sModem = 16 * 1000;
static const NSUInteger kBitrateBad2G = 56 * 1000;
static const NSUInteger kBitrate2G = 128 * 1000; // 2G
static const NSUInteger kBitrate2GPlus = kBitrate2G * 2; // 2.5G
static const NSUInteger kBitrate3G = kBitrate2GPlus * 2; // 3G
static const NSUInteger kBitrate3GPlus = kBitrate3G + kBitrate2GPlus; // 3.5G
static const NSUInteger kBitrate4G = kBitrate3G * 2; // 4G
static const NSUInteger kBitrate4GPlus = kBitrate4G * 2; // ~LTE
static const NSUInteger sBitrates[] = {
kBitrateDribble, kBitrate80sModem, kBitrateBad2G,
kBitrate2G, kBitrate2GPlus,
kBitrate3G, kBitrate3GPlus,
kBitrate4G, kBitrate4GPlus
};
static const NSUInteger kDefaultBitrateIndex = 5;
@interface ViewController () <UIPickerViewDataSource, UIPickerViewDelegate, TIPImageFetchRequest, TIPImageFetchDelegate, TIPImageFetchTransformer>
@end
@implementation ViewController
{
IBOutlet UIImageView *_imageView;
IBOutlet UIProgressView *_progressView;
IBOutlet UIButton *_selectImageTypeButton;
IBOutlet UIButton *_selectSpeedButton;
IBOutlet UIButton *_startButton;
IBOutlet UIPickerView *_pickerView;
IBOutlet UILabel *_resultsLabel;
IBOutlet UISwitch *_blurSwitch;
BOOL _selectingSpeed;
UITapGestureRecognizer *_tapper;
NSUInteger _imageTypeIndex;
NSUInteger _speedIndex;
TIPImagePipeline *_imagePipeline;
id<TIPImageFetchDownloadProviderWithStubbingSupport> _downloadProvider;
TIPImageFetchOperation *_fetchOperation;
CFAbsoluteTime _startTime;
CFAbsoluteTime _firstImageTime;
CFAbsoluteTime _finalImageTime;
NSUInteger _size;
CGSize _cachedBounds;
UIViewContentMode _cachedContentMode;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_progressView.progress = 0;
_imageTypeIndex = 0;
_speedIndex = kDefaultBitrateIndex;
[self hidePickerView:NO];
[self updateImageTypeButtonTitle];
[self updateSpeedButtonTitle];
_tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stopSelecting:)];
_tapper.enabled = NO;
[_imageView addGestureRecognizer:_tapper];
_imagePipeline = [[TIPImagePipeline alloc] initWithIdentifier:@"ImageSpeedComparison"];
_downloadProvider =[[TIPTestImageFetchDownloadProviderInternalWithStubbing alloc] init];
[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider = _downloadProvider;
[_downloadProvider setDownloadStubbingEnabled:YES];
}
- (void)viewDidLayoutSubviews
{
CGRect frame;
frame = _progressView.frame;
frame.origin.y = CGRectGetMaxY(_imageView.frame);
_progressView.frame = frame;
frame = _selectImageTypeButton.frame;
frame.origin.y = CGRectGetMaxY(_progressView.frame) + 5;
_selectImageTypeButton.frame = frame;
frame = _selectSpeedButton.frame;
frame.origin.y = CGRectGetMaxY(_selectImageTypeButton.frame) + 5;
_selectSpeedButton.frame = frame;
frame = _selectSpeedButton.frame;
frame.origin.y += frame.size.height;
frame.size = _blurSwitch.bounds.size;
frame.origin.x = self.view.bounds.size.width - (frame.size.width + 5);
_blurSwitch.frame = frame;
frame = _startButton.frame;
frame.origin.y = CGRectGetMaxY(_selectSpeedButton.frame) + 5;
_startButton.frame = frame;
frame = _resultsLabel.frame;
frame.origin.y = CGRectGetMaxY(_startButton.frame) + 5 ;
_resultsLabel.frame = frame;
[super viewDidLayoutSubviews];
}
#pragma mark Actions
- (IBAction)start:(id)sender
{
if (_fetchOperation) {
return;
}
_startButton.enabled = NO;
_cachedBounds = _imageView.bounds.size;
_cachedContentMode = _imageView.contentMode;
_fetchOperation = [_imagePipeline operationWithRequest:self context:nil delegate:self];
[self registerCannedImage];
[_imagePipeline fetchImageWithOperation:_fetchOperation];
}
- (IBAction)select:(UIButton *)sender
{
if (!_pickerView.userInteractionEnabled) {
_selectingSpeed = (sender == _selectSpeedButton);
[self showPickerView:YES];
_imageView.image = nil;
_progressView.progress = 0;
_startTime = 0;
[self updateResults];
}
}
- (IBAction)stopSelecting:(id)sender
{
if (_pickerView.userInteractionEnabled) {
UIButton *button = (_selectingSpeed) ? _selectSpeedButton : _selectImageTypeButton;
if (!button.enabled) {
[self hidePickerView:YES];
}
}
}
#pragma mark UI
- (void)updateResults
{
if (0 == _startTime) {
_resultsLabel.text = nil;
} else {
NSString *firstResult = @"N/A";
NSString *finalResult = @"N/A";
NSString *totalSize = @"X KBs";
if (0 != _firstImageTime) {
firstResult = [NSString stringWithFormat:@"%.4fs", _firstImageTime - _startTime];
}
if (0 != _finalImageTime) {
finalResult = [NSString stringWithFormat:@"%.4fs", _finalImageTime - _startTime];
}
if (0 != _size) {
totalSize = [NSByteCountFormatter stringFromByteCount:(long long)_size countStyle:NSByteCountFormatterCountStyleBinary];
}
_resultsLabel.text = [NSString stringWithFormat:@"First Scan: %@\nFinal Scan: %@\nFinal Size: %@", firstResult, finalResult, totalSize];
}
}
- (void)updateImageTypeButtonTitle
{
[_selectImageTypeButton setTitle:[NSString stringWithFormat:@"Type: %@", @(sImageTypes[_imageTypeIndex].name)]
forState:UIControlStateNormal];
}
- (void)updateSpeedButtonTitle
{
[_selectSpeedButton setTitle:[NSString stringWithFormat:@"Speed: %tu Kbps", sBitrates[_speedIndex] / 1000] forState:UIControlStateNormal];
}
#pragma mark Canned Image
- (void)registerCannedImage
{
NSURL *cannedImageURL = [self cannedImageFileURL];
NSURL *imageURL = self.imageURL;
if (cannedImageURL.isFileURL) {
NSData *imageData = [NSData dataWithContentsOfURL:cannedImageURL
options:NSDataReadingMappedIfSafe
error:NULL];
[_downloadProvider addDownloadStubForRequestURL:imageURL responseData:imageData responseMIMEType:nil shouldSupportResuming:NO suggestedBitrate:sBitrates[_speedIndex]];
}
}
- (void)unregisterCannedImage
{
NSURL *imageURL = self.imageURL;
[_downloadProvider removeDownloadStubForRequestURL:imageURL];
}
#pragma mark Picker View
- (void)showPickerView:(BOOL)animated
{
_selectImageTypeButton.enabled = NO;
_selectSpeedButton.enabled = NO;
_startButton.enabled = NO;
[_imagePipeline clearMemoryCaches];
[_imagePipeline clearDiskCache];
[_pickerView reloadAllComponents];
[_pickerView selectRow:(_selectingSpeed) ? (NSInteger)_speedIndex : (NSInteger)_imageTypeIndex inComponent:0 animated:NO];
[UIView animateWithDuration:animated ? 0.5 : 0.0
animations:^{
self->_selectImageTypeButton.alpha = 0;
self->_selectSpeedButton.alpha = 0;
self->_startButton.alpha = 0;
self->_resultsLabel.alpha = 0;
CGRect frame = self->_pickerView.frame;
frame.origin.y = self.view.bounds.size.height - frame.size.height;
self->_pickerView.frame = frame;
}
completion:^(BOOL finished) {
self->_pickerView.userInteractionEnabled = YES;
self->_tapper.enabled = YES;
}];
}
- (void)hidePickerView:(BOOL)animated
{
_pickerView.userInteractionEnabled = NO;
[UIView animateWithDuration:animated ? 0.5 : 0.0
animations:^{
self->_selectImageTypeButton.alpha = 1;
self->_selectSpeedButton.alpha = 1;
self->_startButton.alpha = 1;
self->_resultsLabel.alpha = 1;
CGRect frame = self->_pickerView.frame;
frame.origin.y = self.view.bounds.size.height;
self->_pickerView.frame = frame;
}
completion:^(BOOL finished) {
self->_startButton.enabled = YES;
self->_selectImageTypeButton.enabled = YES;
self->_selectSpeedButton.enabled = YES;
self->_tapper.enabled = NO;
}];
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
if (row < 0) {
return;
}
if (_selectingSpeed) {
_speedIndex = (NSUInteger)row;
[self updateSpeedButtonTitle];
} else {
_imageTypeIndex = (NSUInteger)row;
[self updateImageTypeButtonTitle];
}
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
return (_selectingSpeed) ? [NSString stringWithFormat:@"%tu Kbps", sBitrates[row] / 1000] : @(sImageTypes[row].name);
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return (_selectingSpeed) ? (sizeof(sBitrates) / sizeof(sBitrates[0])) : (sizeof(sImageTypes) / sizeof(sImageTypes[0]));
}
#pragma mark Image Fetch Request
- (NSURL *)imageURL
{
NSString *imageName = @(sImageTypes[_imageTypeIndex].file);
NSString *imageURLString;
if ([imageName hasPrefix:@"http"]) {
imageURLString = imageName;
} else {
imageURLString = [NSString stringWithFormat:@"https://www.twitterfied.com/%@", imageName];
}
NSURL *imageURL = [NSURL URLWithString:imageURLString];
return imageURL;
}
- (CGSize)targetDimensions
{
return TIPDimensionsFromPointSize(_cachedBounds);
}
- (UIViewContentMode)targetContentMode
{
return _cachedContentMode;
}
- (id<TIPImageFetchTransformer>)transformer
{
return _blurSwitch.on ? self : nil;
}
- (UIImage *)tip_transformImage:(UIImage *)image withProgress:(float)progress hintTargetDimensions:(CGSize)targetDimensions hintTargetContentMode:(UIViewContentMode)targetContentMode forImageFetchOperation:(TIPImageFetchOperation *)op
{
if (!image.CGImage) {
return nil;
}
BOOL shouldScaleFirst = NO;
const CGSize imageDimension = [image tip_dimensions];
CGFloat blurRadius = 0;
if (progress < 0 || progress >= 1.f) {
// placeholder?
id<TIPImageFetchRequest> request = op.request;
if (![request respondsToSelector:@selector(options)]) {
return nil;
}
if ((request.options & TIPImageFetchTreatAsPlaceholder) == 0) {
return nil;
}
if (targetDimensions.width <= imageDimension.width && targetDimensions.height <= imageDimension.height) {
return nil;
}
blurRadius = (CGFloat)log2(MAX(targetDimensions.height / imageDimension.height, targetDimensions.width / targetDimensions.width));
shouldScaleFirst = YES;
} else {
// progressive
if (progress > .65f) {
return nil;
}
const CGFloat divisor = (1.f + progress) * 2.f;
blurRadius = (CGFloat)log2(MAX(imageDimension.width, imageDimension.height)) / divisor;
blurRadius *= 1.f - progress;
}
if (blurRadius < 0.5) {
return nil;
}
// TRANSFORM!
if (shouldScaleFirst) {
image = [image tip_scaledImageWithTargetDimensions:targetDimensions contentMode:targetContentMode];
}
UIImage *transformed = [image tip_blurredImageWithRadius:blurRadius];
NSAssert(CGSizeEqualToSize([image tip_dimensions], [transformed tip_dimensions]), @"sizing missmatch!");
return transformed;
}
- (NSString *)tip_transformerIdentifier
{
return @"speed.test.transformer";
}
//- (NSDictionary *)progressiveLoadingPolicies
//{
// return @{ TIPImageTypeJPEG : [[TIPGreedyProgressiveLoadingPolicy alloc] init] };
//}
//- (NSDictionary *)progressiveLoadingPolicies
//{
// if (sImageTypes[_imageTypeIndex].isProgressive) {
// if (![sImageTypes[_imageTypeIndex].type isEqualToString:TIPImageTypeJPEG]) {
// return @{ sImageTypes[_imageTypeIndex].type : [[TIPGreedyProgressiveLoadingPolicy alloc] init] };
// }
// }
// return nil;
//}
- (NSDictionary *)progressiveLoadingPolicies
{
return @{ TIPImageTypeWEBP : [[TIPFullFrameProgressiveLoadingPolicy alloc] init] };
}
- (NSURL *)cannedImageFileURL
{
NSString *file = @(sImageTypes[_imageTypeIndex].file);
if ([file hasPrefix:@"http"]) {
return [NSURL URLWithString:file];
}
return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:file ofType:nil]];
}
#pragma mark Image Fetch Operation
- (void)tip_imageFetchOperationDidStart:(TIPImageFetchOperation *)op
{
_progressView.progressTintColor = [UIColor yellowColor];
_progressView.progress = 0;
_imageView.image = nil;
_startTime = 0;
[self updateResults];
_startTime = CFAbsoluteTimeGetCurrent();
_firstImageTime = _finalImageTime = 0;
_size = 0;
}
- (BOOL)tip_imageFetchOperation:(TIPImageFetchOperation *)op
shouldLoadProgressivelyWithIdentifier:(NSString *)identifier
URL:(NSURL *)URL
imageType:(NSString *)imageType
originalDimensions:(CGSize)originalDimensions
{
return sImageTypes[_imageTypeIndex].isProgressive;
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op
didUpdateProgressiveImage:(id<TIPImageFetchResult>)progressiveResult
progress:(float)progress
{
[_progressView setProgress:progress animated:YES];
_imageView.image = progressiveResult.imageContainer.image;
if (0 == _firstImageTime) {
_firstImageTime = CFAbsoluteTimeGetCurrent();
}
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFirstAnimatedImageFrame:(id<TIPImageFetchResult>)progressiveResult progress:(float)progress
{
[_progressView setProgress:progress animated:YES];
_imageView.image = progressiveResult.imageContainer.image;
_progressView.progress = progress;
if (0 == _firstImageTime) {
_firstImageTime = CFAbsoluteTimeGetCurrent();
}
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didUpdateProgress:(float)progress
{
[_progressView setProgress:progress animated:YES];
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFinalImage:(id<TIPImageFetchResult>)finalResult
{
[self unregisterCannedImage];
if (0 == _firstImageTime) {
_firstImageTime = CFAbsoluteTimeGetCurrent();
}
_size = [op.metrics metricInfoForSource:finalResult.imageSource].networkImageSizeInBytes;
_finalImageTime = CFAbsoluteTimeGetCurrent();
_imageView.image = finalResult.imageContainer.image;
[_progressView setProgress:1.f animated:YES];
_progressView.progressTintColor = [UIColor greenColor];
_fetchOperation = nil;
_startButton.enabled = YES;
_selectImageTypeButton.enabled = YES;
[self updateResults];
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didFailToLoadFinalImage:(NSError *)error
{
[self unregisterCannedImage];
_progressView.progressTintColor = [UIColor redColor];
_fetchOperation = nil;
_startButton.enabled = YES;
_selectImageTypeButton.enabled = YES;
[self updateResults];
}
@end