From 8fb045804ad89596c1782b7f69594c86012566d8 Mon Sep 17 00:00:00 2001 From: Alex Quinlivan Date: Fri, 16 Apr 2021 15:34:30 +1200 Subject: [PATCH 1/4] Create example data to show failing un-rotation of baked orientation in pixel data --- Examples/Example-Shared/Kitten.h | 8 +- Examples/Example-Shared/Kitten.m | 6 +- .../Example/Example.xcodeproj/project.pbxproj | 8 ++ Examples/Example/PINRemoteImage/ImageSource.h | 23 ++++ Examples/Example/PINRemoteImage/Oriented.h | 25 ++++ Examples/Example/PINRemoteImage/Oriented.m | 130 ++++++++++++++++++ .../PINRemoteImage/PINViewController.m | 34 +++-- Examples/Example/Podfile.lock | 50 +++---- 8 files changed, 240 insertions(+), 44 deletions(-) create mode 100644 Examples/Example/PINRemoteImage/ImageSource.h create mode 100644 Examples/Example/PINRemoteImage/Oriented.h create mode 100644 Examples/Example/PINRemoteImage/Oriented.m 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 From 6bbf499179eb449e08691747fc1adddde5b86555 Mon Sep 17 00:00:00 2001 From: Alex Quinlivan Date: Fri, 16 Apr 2021 15:41:50 +1200 Subject: [PATCH 2/4] Invert the rotation angle applied to the image so that the resulting image represents 0 rotation When converting 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 --- Source/Classes/Categories/PINImage+DecodedImage.m | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Source/Classes/Categories/PINImage+DecodedImage.m b/Source/Classes/Categories/PINImage+DecodedImage.m index da75e5bc..d82450d9 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; From 0171b2f7e7291f3e8ae33ed3f309a8e2631317b8 Mon Sep 17 00:00:00 2001 From: Alex Quinlivan Date: Mon, 24 May 2021 17:01:19 +1200 Subject: [PATCH 3/4] Manually set graphics image renderer size due to floating point rounding behaving incorrectly after transform applied --- .../Classes/Categories/PINImage+DecodedImage.m | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Source/Classes/Categories/PINImage+DecodedImage.m b/Source/Classes/Categories/PINImage+DecodedImage.m index d82450d9..e53e5a8b 100644 --- a/Source/Classes/Categories/PINImage+DecodedImage.m +++ b/Source/Classes/Categories/PINImage+DecodedImage.m @@ -218,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; From 2b1b06a6740eaf1141c1090ceb148fff3fde6627 Mon Sep 17 00:00:00 2001 From: Alex Quinlivan Date: Mon, 24 May 2021 17:02:42 +1200 Subject: [PATCH 4/4] Update image orientation test to actually test on images with all exif orientations available --- Tests/PINRemoteImageTests.m | 164 +++++++++++++++++++++++++----------- 1 file changed, 116 insertions(+), 48 deletions(-) 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