diff --git a/Examples/Example-Shared/Kitten.h b/Examples/Example-Shared/Kitten.h index 5428710c..6a51cfd5 100644 --- a/Examples/Example-Shared/Kitten.h +++ b/Examples/Example-Shared/Kitten.h @@ -13,12 +13,8 @@ #import #endif -@interface Kitten : NSObject +#import "ImageSource.h" -@property (nonatomic, strong) NSURL *imageURL; -@property (nonatomic, strong) id dominantColor; -@property (nonatomic, assign) CGSize imageSize; - -+ (void)fetchKittenForWidth:(CGFloat)width completion:(void (^)(NSArray *kittens))completion; +@interface Kitten : NSObject @end diff --git a/Examples/Example-Shared/Kitten.m b/Examples/Example-Shared/Kitten.m index f2a48cb3..fdaf098e 100644 --- a/Examples/Example-Shared/Kitten.m +++ b/Examples/Example-Shared/Kitten.m @@ -31,10 +31,12 @@ - (CGSize)CGSizeValue #endif - @implementation Kitten +@synthesize imageURL; +@synthesize dominantColor; +@synthesize imageSize; -+ (void)fetchKittenForWidth:(CGFloat)width completion:(void (^)(NSArray *kittens))completion ++ (void)fetchImagesForWidth:(CGFloat)width completion:(void (^)(NSArray *images))completion { NSArray *kittenURLs = @[[NSURL URLWithString:@"https://i.pinimg.com/736x/92/5d/5a/925d5ac74db0dcfabc238e1686e31d16.jpg"], [NSURL URLWithString:@"https://i.pinimg.com/736x/ff/b3/ae/ffb3ae40533b7f9463cf1c04d7ab69d1.jpg"], diff --git a/Examples/Example/Example.xcodeproj/project.pbxproj b/Examples/Example/Example.xcodeproj/project.pbxproj index e8bc4dd7..7a4c566e 100644 --- a/Examples/Example/Example.xcodeproj/project.pbxproj +++ b/Examples/Example/Example.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 68B3850B1B5572BF004EB26F /* ProgressiveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B3850A1B5572BF004EB26F /* ProgressiveViewController.m */; }; 68B3850E1B5577D4004EB26F /* DegradedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B3850D1B5577D4004EB26F /* DegradedViewController.m */; }; 9D9328E51C3D4CC200E1F1D3 /* Kitten.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D9328E41C3D4CC200E1F1D3 /* Kitten.m */; }; + FABFA41B262938420074812A /* Oriented.m in Sources */ = {isa = PBXBuildFile; fileRef = FABFA41A262938420074812A /* Oriented.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -68,6 +69,9 @@ A5DEC9B706184109844D57E2 /* PINRemoteImage.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = PINRemoteImage.podspec; path = ../PINRemoteImage.podspec; sourceTree = ""; }; E2B84DF860DF48B5B22537B6 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; F30E394524AE56850067A777 /* PINRemoteImage copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "PINRemoteImage copy-Info.plist"; path = "/Users/garrettmoon/code/PINRemoteImage/Examples/Example/PINRemoteImage copy-Info.plist"; sourceTree = ""; }; + FABFA419262938420074812A /* Oriented.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Oriented.h; sourceTree = ""; }; + FABFA41A262938420074812A /* Oriented.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Oriented.m; sourceTree = ""; }; + FABFA41D262938590074812A /* ImageSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImageSource.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -122,8 +126,11 @@ 6003F593195388D20070C39A /* PINRemoteImage */ = { isa = PBXGroup; children = ( + FABFA41D262938590074812A /* ImageSource.h */, 9D9328E31C3D4CC200E1F1D3 /* Kitten.h */, 9D9328E41C3D4CC200E1F1D3 /* Kitten.m */, + FABFA419262938420074812A /* Oriented.h */, + FABFA41A262938420074812A /* Oriented.m */, 6003F59C195388D20070C39A /* PINAppDelegate.h */, 6003F59D195388D20070C39A /* PINAppDelegate.m */, 6003F59F195388D20070C39A /* Main.storyboard */, @@ -287,6 +294,7 @@ buildActionMask = 2147483647; files = ( 68B385081B557116004EB26F /* WebPViewController.m in Sources */, + FABFA41B262938420074812A /* Oriented.m in Sources */, 68B3850B1B5572BF004EB26F /* ProgressiveViewController.m in Sources */, 9D9328E51C3D4CC200E1F1D3 /* Kitten.m in Sources */, 6003F59E195388D20070C39A /* PINAppDelegate.m in Sources */, diff --git a/Examples/Example/PINRemoteImage/ImageSource.h b/Examples/Example/PINRemoteImage/ImageSource.h new file mode 100644 index 00000000..92efddd3 --- /dev/null +++ b/Examples/Example/PINRemoteImage/ImageSource.h @@ -0,0 +1,23 @@ +// +// ImageSource.h +// Example +// +// Created by Alex Quinlivan on 16/04/21. +// Copyright © 2021 Garrett Moon. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ImageSource + +@property (nonatomic, strong) NSURL *imageURL; +@property (nonatomic, strong) id dominantColor; +@property (nonatomic, assign) CGSize imageSize; + ++ (void)fetchImagesForWidth:(CGFloat)width completion:(void (^)(NSArray *images))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Examples/Example/PINRemoteImage/Oriented.h b/Examples/Example/PINRemoteImage/Oriented.h new file mode 100644 index 00000000..1186428f --- /dev/null +++ b/Examples/Example/PINRemoteImage/Oriented.h @@ -0,0 +1,25 @@ +// +// Oriented.h +// Example +// +// Created by Alex Quinlivan on 16/04/21. +// Copyright © 2021 Garrett Moon. All rights reserved. +// + +#import + +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED +#import +#else +#import +#endif + +#import "ImageSource.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface Oriented : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Examples/Example/PINRemoteImage/Oriented.m b/Examples/Example/PINRemoteImage/Oriented.m new file mode 100644 index 00000000..b09d96c4 --- /dev/null +++ b/Examples/Example/PINRemoteImage/Oriented.m @@ -0,0 +1,130 @@ +// +// Oriented.m +// Example +// +// Created by Alex Quinlivan on 16/04/21. +// Copyright © 2021 Garrett Moon. All rights reserved. +// + +#import "Oriented.h" + +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED + +@interface NSValue (PINiOSMapping) ++ (NSValue *)valueWithCGSize:(CGSize)size; +- (CGSize)CGSizeValue; +@end + +@implementation NSValue (PINiOSMapping) + ++ (NSValue *)valueWithCGSize:(CGSize)size +{ + return [self valueWithSize:size]; +} + +- (CGSize)CGSizeValue +{ + return self.sizeValue; +} + +@end + +#endif + +@implementation Oriented +@synthesize imageURL; +@synthesize dominantColor; +@synthesize imageSize; + ++ (void)fetchImagesForWidth:(CGFloat)width completion:(void (^)(NSArray *images))completion +{ + NSArray *orientedURLs = @[[NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_0.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_1.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_2.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_3.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_4.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_5.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_6.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_7.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Landscape_8.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_0.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_1.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_2.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_3.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_4.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_5.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_6.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_7.jpg?raw=true"], + [NSURL URLWithString:@"https://github.com/AlexQuinlivan/exif-orientation-examples/blob/master/Portrait_8.jpg?raw=true"], + ]; + + NSArray *orientedSizes = @[[NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(450, 300)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + [NSValue valueWithCGSize:CGSizeMake(300, 450)], + ]; + + dispatch_group_t group = dispatch_group_create(); + NSMutableArray *orienteds = [[NSMutableArray alloc] init]; + + CGFloat scale = 1; +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + scale = [[UIScreen mainScreen] scale]; +#else + scale = [[NSScreen mainScreen] backingScaleFactor]; +#endif + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSInteger count = 0; + for (NSInteger idx = 0; idx < 500; idx++) { + Oriented *oriented = [[Oriented alloc] init]; + CGFloat r = (rand() % 255) / 255.0f; + CGFloat g = (rand() % 255) / 255.0f; + CGFloat b = (rand() % 255) / 255.0f; +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + oriented.dominantColor = [UIColor colorWithRed:r green:g blue:b alpha:1.0f]; +#else + oriented.dominantColor = [NSColor colorWithRed:r green:g blue:b alpha:1.0f]; +#endif + + NSUInteger orientedIdx = rand() % 18; + + CGSize orientedSize = [orientedSizes[orientedIdx] CGSizeValue]; + NSInteger orientedSizeWidth = orientedSize.width; + NSInteger orientedSizeHeight = orientedSize.height; + + if (orientedSizeWidth > (width * scale)) { + orientedSizeHeight = ((width * scale) / orientedSizeWidth) * orientedSizeHeight; + orientedSizeWidth = (width * scale); + } + + oriented.imageURL = orientedURLs[orientedIdx]; + oriented.imageSize = CGSizeMake(orientedSizeWidth / scale, orientedSizeHeight / scale); + + dispatch_sync(dispatch_get_main_queue(), ^{ + [orienteds addObject:oriented]; + }); + count++; + } + }); + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + if (completion) { + completion(orienteds); + } + }); +} + +@end diff --git a/Examples/Example/PINRemoteImage/PINViewController.m b/Examples/Example/PINRemoteImage/PINViewController.m index 0770d944..37941549 100644 --- a/Examples/Example/PINRemoteImage/PINViewController.m +++ b/Examples/Example/PINRemoteImage/PINViewController.m @@ -12,12 +12,14 @@ #import #import +#import "ImageSource.h" #import "Kitten.h" +#import "Oriented.h" @interface PINViewController () @property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) NSMutableArray *kittens; +@property (nonatomic, strong) NSMutableArray *images; @end @@ -40,8 +42,18 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder - (void)fetchKittenImages { - [Kitten fetchKittenForWidth:CGRectGetWidth(self.collectionView.frame) completion:^(NSArray *kittens) { - [self.kittens addObjectsFromArray:kittens]; + [Kitten fetchImagesForWidth:CGRectGetWidth(self.collectionView.frame) completion:^(NSArray *kittens) { + [self.images removeAllObjects]; + [self.images addObjectsFromArray:kittens]; + [self.collectionView reloadData]; + }]; +} + +- (void)fetchOrientedImages +{ + [Oriented fetchImagesForWidth:CGRectGetWidth(self.collectionView.frame) completion:^(NSArray *oriented) { + [self.images removeAllObjects]; + [self.images addObjectsFromArray:oriented]; [self.collectionView reloadData]; }]; } @@ -56,16 +68,16 @@ - (void)viewDidLoad [self.collectionView registerClass:[PINImageCell class] forCellWithReuseIdentifier:NSStringFromClass([PINImageCell class])]; [self.view addSubview:self.collectionView]; - self.kittens = [[NSMutableArray alloc] init]; - [self fetchKittenImages]; + self.images = [[NSMutableArray alloc] init]; + [self fetchOrientedImages]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { - Kitten *kitten = [self.kittens objectAtIndex:indexPath.item]; - return kitten.imageSize; + id image = [self.images objectAtIndex:indexPath.item]; + return image.imageSize; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView @@ -75,18 +87,18 @@ - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return self.kittens.count; + return self.images.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { PINImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([PINImageCell class]) forIndexPath:indexPath]; - Kitten *kitten = [self.kittens objectAtIndex:indexPath.item]; - cell.backgroundColor = kitten.dominantColor; + id image = [self.images objectAtIndex:indexPath.item]; + cell.backgroundColor = image.dominantColor; cell.imageView.alpha = 0.0f; __weak PINImageCell *weakCell = cell; - [cell.imageView pin_setImageFromURL:kitten.imageURL + [cell.imageView pin_setImageFromURL:image.imageURL completion:^(PINRemoteImageManagerResult *result) { if (result.requestDuration > 0.25) { [UIView animateWithDuration:0.3 animations:^{ diff --git a/Examples/Example/Podfile.lock b/Examples/Example/Podfile.lock index 337741b4..b613a46b 100644 --- a/Examples/Example/Podfile.lock +++ b/Examples/Example/Podfile.lock @@ -1,29 +1,29 @@ PODS: - - libwebp (1.1.0): - - libwebp/demux (= 1.1.0) - - libwebp/mux (= 1.1.0) - - libwebp/webp (= 1.1.0) - - libwebp/demux (1.1.0): + - libwebp (1.2.0): + - libwebp/demux (= 1.2.0) + - libwebp/mux (= 1.2.0) + - libwebp/webp (= 1.2.0) + - libwebp/demux (1.2.0): - libwebp/webp - - libwebp/mux (1.1.0): + - libwebp/mux (1.2.0): - libwebp/demux - - libwebp/webp (1.1.0) - - PINCache (3.0.1-beta.8): - - PINCache/Arc-exception-safe (= 3.0.1-beta.8) - - PINCache/Core (= 3.0.1-beta.8) - - PINCache/Arc-exception-safe (3.0.1-beta.8): + - libwebp/webp (1.2.0) + - PINCache (3.0.3): + - PINCache/Arc-exception-safe (= 3.0.3) + - PINCache/Core (= 3.0.3) + - PINCache/Arc-exception-safe (3.0.3): - PINCache/Core - - PINCache/Core (3.0.1-beta.8): - - PINOperation (~> 1.1.1) - - PINOperation (1.1.2) - - PINRemoteImage (3.0.0): - - PINRemoteImage/PINCache (= 3.0.0) - - PINRemoteImage/Core (3.0.0): + - PINCache/Core (3.0.3): + - PINOperation (~> 1.2.1) + - PINOperation (1.2.1) + - PINRemoteImage (3.0.3): + - PINRemoteImage/PINCache (= 3.0.3) + - PINRemoteImage/Core (3.0.3): - PINOperation - - PINRemoteImage/PINCache (3.0.0): - - PINCache (= 3.0.1-beta.8) + - PINRemoteImage/PINCache (3.0.3): + - PINCache (~> 3.0.3) - PINRemoteImage/Core - - PINRemoteImage/WebP (3.0.0): + - PINRemoteImage/WebP (3.0.3): - libwebp - PINRemoteImage/Core @@ -43,11 +43,11 @@ EXTERNAL SOURCES: :path: "../../" SPEC CHECKSUMS: - libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3 - PINCache: 534fd41d358d828dfdf227a0d327f3673a65e20b - PINOperation: 24b774353ca248fcf87d67b2d61eef42087c125a - PINRemoteImage: e2b89e19fb6e77ffc099f9d9f3b3fe1745e3f9f9 + libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0 + PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 + PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 + PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 PODFILE CHECKSUM: 6be36f4931404348fd582ddb8bd0b80c534561ac -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 diff --git a/Source/Classes/Categories/PINImage+DecodedImage.m b/Source/Classes/Categories/PINImage+DecodedImage.m index da75e5bc..e53e5a8b 100644 --- a/Source/Classes/Categories/PINImage+DecodedImage.m +++ b/Source/Classes/Categories/PINImage+DecodedImage.m @@ -37,11 +37,11 @@ NS_INLINE void pin_degreesFromOrientation(UIImageOrientation orientation, void ( case UIImageOrientationDown: // 180 deg rotation completion(180.0, NO, NO); break; - case UIImageOrientationLeft: - completion(270.0, NO, NO); // 90 deg CCW + case UIImageOrientationLeft: // 90 deg CCW + completion(270.0, NO, NO); break; - case UIImageOrientationRight: - completion(90.0, NO, NO); // 90 deg CW + case UIImageOrientationRight: // 90 deg CW + completion(90.0, NO, NO); break; case UIImageOrientationUpMirrored: // as above but image mirrored along other axis. horizontal flip completion(0.0, YES, NO); @@ -198,8 +198,11 @@ + (PINImage *)pin_decodedImageUsingGraphicsImageRendererRefWithCGImageRef:(CGIma __block BOOL doVerticalFlip = NO; pin_degreesFromOrientation(orientation, ^(CGFloat degrees, BOOL horizontalFlip, BOOL verticalFlip) { - // Convert degrees to radians - radians = [[[NSMeasurement alloc] initWithDoubleValue:degrees + // Convert degrees to radians we want to reverse the degrees calculated from the image + // orientation as they represent the current transformation that is baked into the image. + // When applying the inverse transform, we will receive an image that represents + // UIImageOrientationUp + radians = [[[NSMeasurement alloc] initWithDoubleValue:-degrees unit:[NSUnitAngle degrees]] measurementByConvertingToUnit:[NSUnitAngle radians]].doubleValue; doHorizontalFlip = horizontalFlip; @@ -215,8 +218,22 @@ + (PINImage *)pin_decodedImageUsingGraphicsImageRendererRefWithCGImageRef:(CGIma // Rotate rect by transformation CGRect rotatedRect = CGRectApplyAffineTransform(CGRectMake(0.0, 0.0, imageSize.width, imageSize.height), transform); + // Do not use rotated rect for renderer size as renderer contexts will round up pixel sizes + // and affine transformations when applied to CGFloats have a different rounding tolerance + CGSize contextSize = imageSize; + switch (orientation) { + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + contextSize = CGSizeMake(contextSize.height, contextSize.width); + break; + default: + break; + } + // Use graphics renderer to render image - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rotatedRect.size format:format]; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:contextSize format:format]; return [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { CGContextRef ctx = rendererContext.CGContext; diff --git a/Tests/PINRemoteImageTests.m b/Tests/PINRemoteImageTests.m index 3a772630..98d0edc1 100644 --- a/Tests/PINRemoteImageTests.m +++ b/Tests/PINRemoteImageTests.m @@ -1259,63 +1259,131 @@ - (void)testThatGrayscalePNGImageIsEightBPP - (void)testImageRendererOrientation { - dispatch_group_t group = dispatch_group_create(); - __block CGImageRef imageRefEncoded = nil; - - dispatch_group_enter(group); - [self.imageManager downloadImageWithURL:[self JPEGURL] - options:PINRemoteImageManagerDownloadOptionsSkipDecode - completion:^(PINRemoteImageManagerResult *result) - { - imageRefEncoded = CGImageCreateCopy(result.image.CGImage); - dispatch_group_leave(group); - }]; - - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - + // Generate a 24x48 pixel grid image, with distinct colours at all four corners. + // The initial version will be the reference image + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(NULL, 24, 48, 8, 8 * 24, colorspace, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host); + CGColorSpaceRelease(colorspace); + XCTAssert(ctx != nil, @"Failed to create CGContext"); + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + CGContextSetRGBFillColor(ctx, i, 0, j, 1); + CGContextFillRect(ctx, CGRectMake(i * 12, j * 24, 12, 24)); + } + } + + CGImageRef referenceImageRef = CGBitmapContextCreateImage(ctx); + CGContextRelease(ctx); + XCTAssert(referenceImageRef != nil, @"Failed to create reference image"); + // All image orientations (copied from `UIImage.h`) - UIImageOrientation allOrientations[] = { - UIImageOrientationUp, // default orientation - UIImageOrientationDown, // 180 deg rotation - UIImageOrientationLeft, // 90 deg CCW - UIImageOrientationRight, // 90 deg CW - UIImageOrientationUpMirrored, // as above but image mirrored along other axis. horizontal flip - UIImageOrientationDownMirrored, // horizontal flip - UIImageOrientationLeftMirrored, // vertical flip - UIImageOrientationRightMirrored, // vertical flip - }; + NSArray *allOrientations = @[ + @(UIImageOrientationUp), // default orientation + @(UIImageOrientationDown), // 180 deg rotation + @(UIImageOrientationLeft), // 90 deg CCW + @(UIImageOrientationRight), // 90 deg CW + @(UIImageOrientationUpMirrored), // as above but image mirrored along other axis. horizontal flip + @(UIImageOrientationDownMirrored), // horizontal flip + @(UIImageOrientationLeftMirrored), // vertical flip + @(UIImageOrientationRightMirrored), // vertical flip + ]; + + // Store reference to the reference image as PNG data, this will come in handy when comparing + // the output of the transformations after they've been corrected by the decoder + UIImage *referenceImage = [UIImage imageWithCGImage:referenceImageRef + scale:1 + orientation:UIImageOrientationUp]; + NSData *referenceImagePNGData = UIImagePNGRepresentation(referenceImage); + CGImageRelease(referenceImageRef); + + // For all image orientations apply the inverse of the corrective transformations + // as if it were how the pixel grid were laid out in the image data. + NSMutableDictionary *imageMap = [NSMutableDictionary new]; + for (NSNumber *orientation in allOrientations) { + CGSize outSize; + switch (orientation.integerValue) { + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + outSize = CGSizeMake(48, 24); + break; + default: + outSize = CGSizeMake(24, 48); + break; + } + UIGraphicsBeginImageContextWithOptions(outSize, true, 1); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(ctx, outSize.width / 2, outSize.height / 2); + + switch (orientation.integerValue) { + case UIImageOrientationDown: + case UIImageOrientationDownMirrored: + if (orientation.integerValue == UIImageOrientationDown) { + CGContextScaleCTM(ctx, 1, -1); + } else { + CGContextScaleCTM(ctx, -1, -1); + } + CGContextRotateCTM(ctx, M_PI); + CGContextTranslateCTM(ctx, -outSize.width / 2, -outSize.height / 2); + break; + case UIImageOrientationLeft: + case UIImageOrientationLeftMirrored: + if (orientation.integerValue != UIImageOrientationLeftMirrored) { + CGContextScaleCTM(ctx, -1, 1); + } + CGContextRotateCTM(ctx, M_PI_2); + CGContextTranslateCTM(ctx, -outSize.height / 2, -outSize.width / 2); + break; + case UIImageOrientationRight: + case UIImageOrientationRightMirrored: + if (orientation.integerValue != UIImageOrientationRightMirrored) { + CGContextScaleCTM(ctx, -1, 1); + } + CGContextRotateCTM(ctx, -M_PI_2); + CGContextTranslateCTM(ctx, -outSize.height / 2, -outSize.width / 2); + break; + case UIImageOrientationUp: + case UIImageOrientationUpMirrored: + if (orientation.integerValue == UIImageOrientationUp) { + CGContextScaleCTM(ctx, 1, -1); + } else { + CGContextScaleCTM(ctx, -1, -1); + } + CGContextTranslateCTM(ctx, -outSize.width / 2, -outSize.height / 2); + break; + default: + XCTFail(@"Unexpected orientation case"); + } + + CGContextDrawImage(ctx, CGRectMake(0, 0, referenceImage.size.width, referenceImage.size.height), referenceImageRef); + UIImage *transformed = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + XCTAssert(transformed != nil, @"Failed to create transformed image for orientation %@", orientation.debugDescription); + imageMap[orientation] = transformed; + } // iOS or tvOS versions below 10.0 use the traditional `+[UIImage imageWithCGImage:]` API that doesn't translate orientation. // For iOS/tvOS 10.0+ we manually convert the `UIImageOrientation` in `UIGraphicsImageRenderer`, and therefore, // the following test is meant to solidify that behavior if (@available(iOS 10.0, tvOS 10.0, *)) { - // Loop over all orientations and compare each element respective to one-another - for (NSInteger i = 0; i < sizeof(allOrientations)/sizeof(allOrientations[0]); i++) { - - // Rotate the reference image by the given orientation - UIImage *referenceImage = [UIImage pin_decodedImageWithCGImageRef:imageRefEncoded orientation:allOrientations[i]]; + // Loop over all orientations and compare each element to the reference image + for (NSNumber *orientation in allOrientations) { + // Apply the decoder's rotation on this orientation + UIImage *decodedImage = [UIImage pin_decodedImageWithCGImageRef:[imageMap[orientation] CGImage] + orientation:orientation.integerValue]; + + // Anything decoded should behave as up + XCTAssert(decodedImage.imageOrientation == UIImageOrientationUp, + @"Decode should've set the output image's rotation to up"); - // Compare the reference image to each element - for (NSInteger j = 0; j < sizeof(allOrientations)/sizeof(allOrientations[0]); j++) { - - // Rotate the image by the given orientation - UIImage *rotatedImage = [UIImage pin_decodedImageWithCGImageRef:imageRefEncoded orientation:allOrientations[j]]; - - // equal images must succeed - if (i == j) { - XCTAssert([UIImageJPEGRepresentation(referenceImage, 1.0) isEqualToData:UIImageJPEGRepresentation(rotatedImage, 1.0)], - @"Unsuccessful transformation. The `referenceImage` and `rotatedImage` are not the same."); - } - // unequal images must fail - else { - XCTAssertFalse([UIImageJPEGRepresentation(referenceImage, 1.0) isEqualToData:UIImageJPEGRepresentation(rotatedImage, 1.0)], - @"Unsuccessful transformation. The `referenceImage` and `rotatedImage` are the same."); - } - } + // Compare pixel content by converting image into lossless data + XCTAssert([UIImagePNGRepresentation(decodedImage) isEqual:referenceImagePNGData], + @"Unsuccessful transformation. The `referenceImage` and `rotatedImage` are not the same."); } } - - CGImageRelease(imageRefEncoded); } - (void)testExponentialRetryStrategy