From 800915e61a7f492fa62da4acd831fa1a4ff5ddf9 Mon Sep 17 00:00:00 2001 From: Ivan <6350992+bivant@users.noreply.github.com> Date: Sat, 30 Mar 2024 03:08:15 +0300 Subject: [PATCH] Add method to flip image horizontally. Update the example projects to have a button that triggers this function. --- .../Categories/UIImage+CropRotate.h | 2 + .../Categories/UIImage+CropRotate.m | 10 ++- .../Models/TOActivityCroppedImageProvider.h | 3 +- .../Models/TOActivityCroppedImageProvider.m | 6 +- .../Models/TOCroppedImageAttributes.h | 3 +- .../Models/TOCroppedImageAttributes.m | 5 +- .../TOCropViewController.h | 32 +++++++-- .../TOCropViewController.m | 50 ++++++++----- .../TOCropViewController/Views/TOCropView.h | 7 ++ .../TOCropViewController/Views/TOCropView.m | 71 ++++++++++++++----- .../ViewController.m | 48 ++++++++++--- .../CropViewController.swift | 52 +++++++++----- .../ViewController.swift | 62 +++++++++++----- 13 files changed, 261 insertions(+), 90 deletions(-) diff --git a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h index a03b8143..4fcacfc9 100644 --- a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h +++ b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.h @@ -29,9 +29,11 @@ NS_ASSUME_NONNULL_BEGIN /// Crops a portion of an existing image object and returns it as a new image /// @param frame The region inside the image (In image pixel space) to crop /// @param angle If any, the angle the image is rotated at as well +/// @param flipped If image should be flipped horizontally /// @param circular Whether the resulting image is returned as a square or a circle - (nonnull UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle + flippedHorizontally:(BOOL)flipped circularClip:(BOOL)circular; @end diff --git a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m index 30daf031..79ce4118 100644 --- a/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m +++ b/Objective-C/TOCropViewController/Categories/UIImage+CropRotate.m @@ -31,7 +31,7 @@ - (BOOL)hasAlpha alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaPremultipliedLast); } -- (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circularClip:(BOOL)circular +- (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle flippedHorizontally:(BOOL)flipped circularClip:(BOOL)circular { UIImage *croppedImage = nil; UIGraphicsBeginImageContextWithOptions(frame.size, !self.hasAlpha && !circular, self.scale); @@ -63,7 +63,13 @@ - (UIImage *)croppedImageWithFrame:(CGRect)frame angle:(NSInteger)angle circular // Perform the rotation transformation CGContextRotateCTM(context, rotation); } - + + if (flipped) + { + CGContextScaleCTM(context, -1, 1); + CGContextTranslateCTM(context, -self.size.width, 0); + } + // Draw the image with all of the transformation parameters applied. // We do not need to worry about specifying the size here since we're already // constrained by the context image size diff --git a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h index b62eb977..f39f3bb0 100644 --- a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h +++ b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.h @@ -29,9 +29,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonnull, nonatomic, readonly) UIImage *image; @property (nonatomic, readonly) CGRect cropFrame; @property (nonatomic, readonly) NSInteger angle; +@property (nonatomic, readonly) BOOL flippedHorizontally; @property (nonatomic, readonly) BOOL circular; -- (nonnull instancetype)initWithImage:(nonnull UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle circular:(BOOL)circular; +- (nonnull instancetype)initWithImage:(nonnull UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle flipped:(BOOL)isFlipped circular:(BOOL)circular; @end diff --git a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m index 76214baf..6c94f305 100644 --- a/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m +++ b/Objective-C/TOCropViewController/Models/TOActivityCroppedImageProvider.m @@ -28,6 +28,7 @@ @interface TOActivityCroppedImageProvider () @property (nonatomic, strong, readwrite) UIImage *image; @property (nonatomic, assign, readwrite) CGRect cropFrame; @property (nonatomic, assign, readwrite) NSInteger angle; +@property (nonatomic, assign, readwrite) BOOL flippedHorizontally; @property (nonatomic, assign, readwrite) BOOL circular; @property (atomic, strong) UIImage *croppedImage; @@ -36,12 +37,13 @@ @interface TOActivityCroppedImageProvider () @implementation TOActivityCroppedImageProvider -- (instancetype)initWithImage:(UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle circular:(BOOL)circular +- (instancetype)initWithImage:(UIImage *)image cropFrame:(CGRect)cropFrame angle:(NSInteger)angle flipped:(BOOL)isFlipped circular:(BOOL)circular { if (self = [super initWithPlaceholderItem:[UIImage new]]) { _image = image; _cropFrame = cropFrame; _angle = angle; + _flippedHorizontally = isFlipped; _circular = circular; } @@ -68,7 +70,7 @@ - (id)item return self.croppedImage; } - UIImage *image = [self.image croppedImageWithFrame:self.cropFrame angle:self.angle circularClip:self.circular]; + UIImage *image = [self.image croppedImageWithFrame:self.cropFrame angle:self.angle flippedHorizontally:self.flippedHorizontally circularClip:self.circular]; self.croppedImage = image; return self.croppedImage; } diff --git a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h index f49f56a7..65dfee88 100644 --- a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h +++ b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.h @@ -28,10 +28,11 @@ NS_ASSUME_NONNULL_BEGIN @interface TOCroppedImageAttributes : NSObject @property (nonatomic, readonly) NSInteger angle; +@property (nonatomic, readonly) BOOL isFlippedHorizontally; @property (nonatomic, readonly) CGRect croppedFrame; @property (nonatomic, readonly) CGSize originalImageSize; -- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle originalImageSize:(CGSize)originalSize; +- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle flippedHorizontally:(BOOL)flipped originalImageSize:(CGSize)originalSize; @end diff --git a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m index bed22c23..85d7cf97 100644 --- a/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m +++ b/Objective-C/TOCropViewController/Models/TOCroppedImageAttributes.m @@ -25,6 +25,7 @@ @interface TOCroppedImageAttributes () @property (nonatomic, assign, readwrite) NSInteger angle; +@property (nonatomic, assign, readwrite) BOOL isFlippedHorizontally; @property (nonatomic, assign, readwrite) CGRect croppedFrame; @property (nonatomic, assign, readwrite) CGSize originalImageSize; @@ -32,10 +33,12 @@ @interface TOCroppedImageAttributes () @implementation TOCroppedImageAttributes -- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle originalImageSize:(CGSize)originalSize +- (instancetype)initWithCroppedFrame:(CGRect)croppedFrame angle:(NSInteger)angle + flippedHorizontally:(BOOL)flipped originalImageSize:(CGSize)originalSize { if (self = [super init]) { _angle = angle; + _isFlippedHorizontally = flipped; _croppedFrame = croppedFrame; _originalImageSize = originalSize; } diff --git a/Objective-C/TOCropViewController/TOCropViewController.h b/Objective-C/TOCropViewController/TOCropViewController.h index 044b9646..dc001cfa 100755 --- a/Objective-C/TOCropViewController/TOCropViewController.h +++ b/Objective-C/TOCropViewController/TOCropViewController.h @@ -44,7 +44,8 @@ */ - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropImageToRect:(CGRect)cropRect - angle:(NSInteger)angle; + angle:(NSInteger)angle + flipped:(BOOL)flipped; /** Called when the user has committed the crop action, and provides @@ -56,7 +57,8 @@ */ - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropToImage:(nonnull UIImage *)image withRect:(CGRect)cropRect - angle:(NSInteger)angle; + angle:(NSInteger)angle + flipped:(BOOL)flipped; /** If the cropping style is set to circular, implementing this delegate will return a circle-cropped version of the selected @@ -68,7 +70,8 @@ */ - (void)cropViewController:(nonnull TOCropViewController *)cropViewController didCropToCircularImage:(nonnull UIImage *)image withRect:(CGRect)cropRect - angle:(NSInteger)angle; + angle:(NSInteger)angle + flipped:(BOOL)flipped; /** If implemented, when the user hits cancel, or completes a @@ -131,6 +134,14 @@ */ @property (nonatomic, assign) NSInteger angle; +/** + Indicates if the image is flipped around vertical axis + + This property can be set before the controller is presented to have + the image 'restored' to a previous cropping layout. + */ +@property (nonatomic, assign) BOOL flippedHorizontally; + /** The toolbar view managed by this view controller. */ @@ -327,7 +338,7 @@ (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropImageToRect)(CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropImageToRect)(CGRect cropRect, NSInteger angle, BOOL flipped); /** Called when the user has committed the crop action, and provides @@ -338,7 +349,7 @@ (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropToRect)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropToRect)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle, BOOL flipped); /** If the cropping style is set to circular, this block will return a circle-cropped version of the selected @@ -349,7 +360,7 @@ (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ -@property (nullable, nonatomic, strong) void (^onDidCropToCircleImage)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle); +@property (nullable, nonatomic, strong) void (^onDidCropToCircleImage)(UIImage* _Nonnull image, CGRect cropRect, NSInteger angle, BOOL flipped); ///------------------------------------------------ @@ -390,6 +401,11 @@ */ - (void)setAspectRatioPreset:(TOCropViewControllerAspectRatioPreset)aspectRatioPreset animated:(BOOL)animated NS_SWIFT_NAME(setAspectRatioPreset(_:animated:)); +/** + Flips image around vertical axis as if user pressed flip button in the upper right corner themself + */ +- (void)flipImageHorizontally; + /** Play a custom animation of the target image zooming to its position in the crop controller while the background fades in. @@ -418,6 +434,7 @@ @param fromView A view that's frame will be used as the origin for this animation. Optional if `fromFrame` has a value. @param fromFrame In the screen's coordinate space, the frame from which the image should animate from. @param angle The rotation angle in which the image was rotated when it was originally cropped. + @param flipped Was the image flipped on the x axis. @param toFrame In the image's coordinate space, the previous crop frame that created the previous crop @param setup A block that is called just before the transition starts. Recommended for hiding any necessary image views. @param completion A block that is called once the transition animation is completed. @@ -427,9 +444,10 @@ fromView:(nullable UIView *)fromView fromFrame:(CGRect)fromFrame angle:(NSInteger)angle + flippedHorizontally:(BOOL)flipped toImageFrame:(CGRect)toFrame setup:(nullable void (^)(void))setup - completion:(nullable void (^)(void))completion NS_SWIFT_NAME(presentAnimatedFrom(_:fromImage:fromView:fromFrame:angle:toFrame:setup:completion:)); + completion:(nullable void (^)(void))completion NS_SWIFT_NAME(presentAnimatedFrom(_:fromImage:fromView:fromFrame:angle:flippedHorizontally:toFrame:setup:completion:)); /** Play a custom animation of the supplied cropped image zooming out from diff --git a/Objective-C/TOCropViewController/TOCropViewController.m b/Objective-C/TOCropViewController/TOCropViewController.m index ce5c7182..28c841c3 100755 --- a/Objective-C/TOCropViewController/TOCropViewController.m +++ b/Objective-C/TOCropViewController/TOCropViewController.m @@ -684,6 +684,11 @@ - (void)setAspectRatioPreset:(TOCropViewControllerAspectRatioPreset)aspectRatioP [self.cropView setAspectRatio:aspectRatio animated:animated]; } +- (void)flipImageHorizontally +{ + [self.cropView flipImageHorizontally]; +} + - (void)rotateCropViewClockwise { [self.cropView rotateImageNinetyDegreesAnimated:YES clockwise:YES]; @@ -713,7 +718,7 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll completion:(void (^)(void))completion { [self presentAnimatedFromParentViewController:viewController fromImage:nil fromView:fromView fromFrame:fromFrame - angle:0 toImageFrame:CGRectZero setup:setup completion:completion]; + angle:0 flippedHorizontally:NO toImageFrame:CGRectZero setup:setup completion:completion]; } - (void)presentAnimatedFromParentViewController:(UIViewController *)viewController @@ -721,6 +726,7 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll fromView:(UIView *)fromView fromFrame:(CGRect)fromFrame angle:(NSInteger)angle + flippedHorizontally:(BOOL)flipped toImageFrame:(CGRect)toFrame setup:(void (^)(void))setup completion:(void (^)(void))completion @@ -734,6 +740,7 @@ - (void)presentAnimatedFromParentViewController:(UIViewController *)viewControll self.angle = angle; self.imageCropFrame = toFrame; } + self.flippedHorizontally = flipped; __weak typeof (self) weakSelf = self; [viewController presentViewController:self.parentViewController ? self.parentViewController : self @@ -904,12 +911,13 @@ - (void)dismissCropViewController - (void)doneButtonTapped { CGRect cropFrame = self.cropView.imageCropFrame; - NSInteger angle = self.cropView.angle; + NSInteger angle = self.angle; + BOOL flipped = self.flippedHorizontally; //If desired, when the user taps done, show an activity sheet if (self.showActivitySheetOnDone) { - TOActivityCroppedImageProvider *imageItem = [[TOActivityCroppedImageProvider alloc] initWithImage:self.image cropFrame:cropFrame angle:angle circular:(self.croppingStyle == TOCropViewCroppingStyleCircular)]; - TOCroppedImageAttributes *attributes = [[TOCroppedImageAttributes alloc] initWithCroppedFrame:cropFrame angle:angle originalImageSize:self.image.size]; + TOActivityCroppedImageProvider *imageItem = [[TOActivityCroppedImageProvider alloc] initWithImage:self.image cropFrame:cropFrame angle:angle flipped:flipped circular:(self.croppingStyle == TOCropViewCroppingStyleCircular)]; + TOCroppedImageAttributes *attributes = [[TOCroppedImageAttributes alloc] initWithCroppedFrame:cropFrame angle:angle flippedHorizontally:flipped originalImageSize:self.image.size]; NSMutableArray *activityItems = [@[imageItem, attributes] mutableCopy]; if (self.activityItems) { @@ -961,35 +969,35 @@ - (void)doneButtonTapped BOOL isCallbackOrDelegateHandled = NO; //If the delegate/block that only supplies crop data is provided, call it - if ([self.delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:)]) { - [self.delegate cropViewController:self didCropImageToRect:cropFrame angle:angle]; + if ([self.delegate respondsToSelector:@selector(cropViewController:didCropImageToRect:angle:flipped:)]) { + [self.delegate cropViewController:self didCropImageToRect:cropFrame angle:angle flipped:flipped]; isCallbackOrDelegateHandled = YES; } if (self.onDidCropImageToRect != nil) { - self.onDidCropImageToRect(cropFrame, angle); + self.onDidCropImageToRect(cropFrame, angle, flipped); isCallbackOrDelegateHandled = YES; } // Check if the circular APIs were implemented - BOOL isCircularImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:)]; + BOOL isCircularImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToCircularImage:withRect:angle:flipped:)]; BOOL isCircularImageCallbackAvailable = self.onDidCropToCircleImage != nil; // Check if non-circular was implemented - BOOL isDidCropToImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:)]; + BOOL isDidCropToImageDelegateAvailable = [self.delegate respondsToSelector:@selector(cropViewController:didCropToImage:withRect:angle:flipped:)]; BOOL isDidCropToImageCallbackAvailable = self.onDidCropToRect != nil; //If cropping circular and the circular generation delegate/block is implemented, call it if (self.croppingStyle == TOCropViewCroppingStyleCircular && (isCircularImageDelegateAvailable || isCircularImageCallbackAvailable)) { - UIImage *image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:YES]; + UIImage *image = [self.image croppedImageWithFrame:cropFrame angle:angle flippedHorizontally:flipped circularClip:YES]; //Dispatch on the next run-loop so the animation isn't interuppted by the crop operation dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (isCircularImageDelegateAvailable) { - [self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle]; + [self.delegate cropViewController:self didCropToCircularImage:image withRect:cropFrame angle:angle flipped:flipped]; } if (isCircularImageCallbackAvailable) { - self.onDidCropToCircleImage(image, cropFrame, angle); + self.onDidCropToCircleImage(image, cropFrame, angle, flipped); } }); @@ -998,21 +1006,21 @@ - (void)doneButtonTapped //If the delegate/block that requires the specific cropped image is provided, call it else if (isDidCropToImageDelegateAvailable || isDidCropToImageCallbackAvailable) { UIImage *image = nil; - if (angle == 0 && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size})) { + if (angle == 0 && CGRectEqualToRect(cropFrame, (CGRect){CGPointZero, self.image.size}) && !flipped) { image = self.image; } else { - image = [self.image croppedImageWithFrame:cropFrame angle:angle circularClip:NO]; + image = [self.image croppedImageWithFrame:cropFrame angle:angle flippedHorizontally:flipped circularClip:NO]; } //Dispatch on the next run-loop so the animation isn't interuppted by the crop operation dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (isDidCropToImageDelegateAvailable) { - [self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle]; + [self.delegate cropViewController:self didCropToImage:image withRect:cropFrame angle:angle flipped:flipped]; } if (isDidCropToImageCallbackAvailable) { - self.onDidCropToRect(image, cropFrame, angle); + self.onDidCropToRect(image, cropFrame, angle, flipped); } }); @@ -1215,6 +1223,16 @@ - (NSInteger)angle return self.cropView.angle; } +- (void)setFlippedHorizontally:(BOOL)flipped +{ + self.cropView.flippedHorizontally = flipped; +} + +- (BOOL)flippedHorizontally +{ + return self.cropView.flippedHorizontally; +} + - (void)setImageCropFrame:(CGRect)imageCropFrame { self.cropView.imageCropFrame = imageCropFrame; diff --git a/Objective-C/TOCropViewController/Views/TOCropView.h b/Objective-C/TOCropViewController/Views/TOCropView.h index a6426361..bcde8f9e 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.h +++ b/Objective-C/TOCropViewController/Views/TOCropView.h @@ -139,6 +139,8 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) NSInteger angle; +@property (nonatomic, assign) BOOL flippedHorizontally; + /** Hide all of the crop elements for transition animations */ @@ -291,6 +293,11 @@ The minimum croping aspect ratio. If set, user is prevented from setting croppin */ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated; +/** + Flips image around vertical axis + */ +- (void)flipImageHorizontally; + @end NS_ASSUME_NONNULL_END diff --git a/Objective-C/TOCropViewController/Views/TOCropView.m b/Objective-C/TOCropViewController/Views/TOCropView.m index adf68d9e..f277874c 100755 --- a/Objective-C/TOCropViewController/Views/TOCropView.m +++ b/Objective-C/TOCropViewController/Views/TOCropView.m @@ -103,6 +103,7 @@ @interface TOCropView () values until the view is configured for the first time. */ @property (nonatomic, assign) NSInteger restoreAngle; @property (nonatomic, assign) CGRect restoreImageCropFrame; +@property (nonatomic, assign) BOOL restoreHorizontalFlip; /* Set to YES once `performInitialLayout` is called. This lets pending properties get queued until the view has been properly set up in its parent. */ @@ -145,6 +146,7 @@ - (void)setup self.resetAspectRatioEnabled = !circularMode; self.restoreImageCropFrame = CGRectZero; self.restoreAngle = 0; + self.restoreHorizontalFlip = NO; self.cropAdjustingDelay = kTOCropTimerDuration; self.cropViewPadding = kTOCropViewPadding; self.maximumZoomScale = kTOMaximumZoomScale; @@ -259,7 +261,11 @@ - (void)performInitialSetup self.restoreAngle = 0; self.cropBoxLastEditedAngle = self.angle; } - + + if (self.restoreHorizontalFlip) { + self.flippedHorizontally = YES; + } + //If an image crop frame was also specified before creation, apply it now if (!CGRectIsEmpty(self.restoreImageCropFrame)) { self.imageCropFrame = self.restoreImageCropFrame; @@ -702,9 +708,10 @@ - (void)resetLayoutToDefaultAnimated:(BOOL)animated _aspectRatio = CGSizeZero; } - if (animated == NO || self.angle != 0) { + if (animated == NO || self.angle != 0 || self.flippedHorizontally) { //Reset all of the rotation transforms _angle = 0; + _flippedHorizontally = NO; //Set the scroll to 1.0f to reset the transform scale self.scrollView.zoomScale = 1.0f; @@ -1234,6 +1241,27 @@ - (void)setAngle:(NSInteger)angle } } +- (CGAffineTransform)imageViewTransform +{ + return CGAffineTransformScale(CGAffineTransformRotate(CGAffineTransformIdentity, self.angleInRadians), _flippedHorizontally ? -1 : 1, 1); +} + +- (void)setFlippedHorizontally:(BOOL)flippedHorizontally +{ + _flippedHorizontally = flippedHorizontally; + if (!self.initialSetupPerformed) { + self.restoreHorizontalFlip = flippedHorizontally; + return; + } + + CGAffineTransform transform = self.imageViewTransform; + + self.backgroundImageView.transform = transform; + self.foregroundImageView.transform = transform; + + [self checkForCanReset]; +} + #pragma mark - Editing Mode - - (void)startEditing { @@ -1369,6 +1397,11 @@ - (void)moveCroppedContentToCenterAnimated:(BOOL)animated }); } +- (void)flipImageHorizontally +{ + self.flippedHorizontally = !self.flippedHorizontally; +} + - (void)setSimpleRenderMode:(BOOL)simpleMode animated:(BOOL)animated { if (simpleMode == _simpleRenderMode) @@ -1535,21 +1568,9 @@ - (void)rotateImageNinetyDegreesAnimated:(BOOL)animated clockwise:(BOOL)clockwis } _angle = newAngle; - - //Convert the new angle to radians - CGFloat angleInRadians = 0.0f; - switch (newAngle) { - case 90: angleInRadians = M_PI_2; break; - case -90: angleInRadians = -M_PI_2; break; - case 180: angleInRadians = M_PI; break; - case -180: angleInRadians = -M_PI; break; - case 270: angleInRadians = (M_PI + M_PI_2); break; - case -270: angleInRadians = -(M_PI + M_PI_2); break; - default: break; - } - + // Set up the transformation matrix for the rotation - CGAffineTransform rotation = CGAffineTransformRotate(CGAffineTransformIdentity, angleInRadians); + CGAffineTransform rotation = self.imageViewTransform; //Work out how much we'll need to scale everything to fit to the new rotation CGRect contentBounds = self.contentBounds; @@ -1703,6 +1724,9 @@ - (void)checkForCanReset if (self.angle != 0) { //Image has been rotated canReset = YES; } + if (self.flippedHorizontally) { + canReset = YES; + } else if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale + FLT_EPSILON) { //image has been zoomed in canReset = YES; } @@ -1744,4 +1768,19 @@ - (BOOL)hasAspectRatio return (self.aspectRatio.width > FLT_EPSILON && self.aspectRatio.height > FLT_EPSILON); } +- (CGFloat)angleInRadians +{ + CGFloat angleInRadians = 0.0f; + switch (_angle) { + case 90: angleInRadians = M_PI_2; break; + case -90: angleInRadians = -M_PI_2; break; + case 180: angleInRadians = M_PI; break; + case -180: angleInRadians = -M_PI; break; + case 270: angleInRadians = (M_PI + M_PI_2); break; + case -270: angleInRadians = -(M_PI + M_PI_2); break; + default: break; + } + return angleInRadians; +} + @end diff --git a/Objective-C/TOCropViewControllerExample/ViewController.m b/Objective-C/TOCropViewControllerExample/ViewController.m index 547ce7e5..ef5e1a8c 100644 --- a/Objective-C/TOCropViewControllerExample/ViewController.m +++ b/Objective-C/TOCropViewControllerExample/ViewController.m @@ -17,6 +17,7 @@ @interface ViewController () * buttonConstraints = @[ + [NSLayoutConstraint constraintWithItem:flipButton attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:cropController.view attribute:NSLayoutAttributeTrailing multiplier:1 constant:-20], + [NSLayoutConstraint constraintWithItem:flipButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:cropController.view attribute:NSLayoutAttributeTop multiplier:1 constant:20], + ]; + [NSLayoutConstraint activateConstraints:buttonConstraints]; +} + #pragma mark - Gesture Recognizer - - (void)didTapImageView { @@ -106,20 +123,24 @@ - (void)didTapImageView fromView:nil fromFrame:viewFrame angle:self.angle + flippedHorizontally:self.isFlippedHorizontally toImageFrame:self.croppedFrame setup:^{ self.imageView.hidden = YES; } - completion:nil]; + completion:^{ + [self addFlipButtonTo:cropController]; + }]; } #pragma mark - Cropper Delegate - -- (void)cropViewController:(TOCropViewController *)cropViewController didCropToImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle +- (void)cropViewController:(TOCropViewController *)cropViewController didCropToImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle flipped:(BOOL)flipped { self.croppedFrame = cropRect; self.angle = angle; + self.isFlippedHorizontally = flipped; [self updateImageViewWithImage:image fromCropViewController:cropViewController]; } -- (void)cropViewController:(TOCropViewController *)cropViewController didCropToCircularImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle +- (void)cropViewController:(TOCropViewController *)cropViewController didCropToCircularImage:(UIImage *)image withRect:(CGRect)cropRect angle:(NSInteger)angle flipped:(BOOL)flipped { self.croppedFrame = cropRect; self.angle = angle; @@ -189,13 +210,7 @@ - (void)showCropViewController UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Crop Image", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - self.croppingStyle = TOCropViewCroppingStyleDefault; - - UIImagePickerController *standardPicker = [[UIImagePickerController alloc] init]; - standardPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - standardPicker.allowsEditing = NO; - standardPicker.delegate = self; - [self presentViewController:standardPicker animated:YES completion:nil]; + [self cropImage]; }]; UIAlertAction *profileAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Make Profile Picture", @"") @@ -270,4 +285,15 @@ - (void)viewDidLayoutSubviews [self layoutImageView]; } +- (void)cropImage +{ + self.croppingStyle = TOCropViewCroppingStyleDefault; + + UIImagePickerController *standardPicker = [[UIImagePickerController alloc] init]; + standardPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + standardPicker.allowsEditing = NO; + standardPicker.delegate = self; + [self presentViewController:standardPicker animated:YES completion:nil]; +} + @end diff --git a/Swift/CropViewController/CropViewController.swift b/Swift/CropViewController/CropViewController.swift index 58b80a0a..31c79ae1 100644 --- a/Swift/CropViewController/CropViewController.swift +++ b/Swift/CropViewController/CropViewController.swift @@ -51,7 +51,7 @@ public typealias CropViewCroppingStyle = TOCropViewCroppingStyle @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - @objc optional func cropViewController(_ cropViewController: CropViewController, didCropImageToRect cropRect: CGRect, angle: Int) + @objc optional func cropViewController(_ cropViewController: CropViewController, didCropImageToRect cropRect: CGRect, angle: Int, flipped: Bool) /** Called when the user has committed the crop action, and provides @@ -61,7 +61,7 @@ public typealias CropViewCroppingStyle = TOCropViewCroppingStyle @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - @objc optional func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) + @objc optional func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int, flipped: Bool) /** If the cropping style is set to circular, implementing this delegate will return a circle-cropped version of the selected @@ -71,7 +71,7 @@ public typealias CropViewCroppingStyle = TOCropViewCroppingStyle @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - @objc optional func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) + @objc optional func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int, flipped: Bool) /** If implemented, when the user hits cancel, or completes a @@ -144,6 +144,17 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { get { return toCropViewController.angle } } + /** + Indicates if the image is flipped around vertical axis + + This property can be set before the controller is presented to have + the image 'restored' to a previous cropping layout. + */ + public var flippedHorizontally: Bool { + set { toCropViewController.flippedHorizontally = newValue } + get { toCropViewController.flippedHorizontally } + } + /** The cropping style of this particular crop view controller */ @@ -337,7 +348,7 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - public var onDidCropImageToRect: ((CGRect, Int) -> (Void))? { + public var onDidCropImageToRect: ((CGRect, Int, Bool) -> (Void))? { set { toCropViewController.onDidCropImageToRect = newValue } get { return toCropViewController.onDidCropImageToRect } } @@ -350,7 +361,7 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - public var onDidCropToRect: ((UIImage, CGRect, NSInteger) -> (Void))? { + public var onDidCropToRect: ((UIImage, CGRect, NSInteger, Bool) -> (Void))? { set { toCropViewController.onDidCropToRect = newValue } get { return toCropViewController.onDidCropToRect } } @@ -363,7 +374,7 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { @param cropRect A rectangle indicating the crop region of the image the user chose (In the original image's local co-ordinate space) @param angle The angle of the image when it was cropped */ - public var onDidCropToCircleImage: ((UIImage, CGRect, NSInteger) -> (Void))? { + public var onDidCropToCircleImage: ((UIImage, CGRect, NSInteger, Bool) -> (Void))? { set { toCropViewController.onDidCropToCircleImage = newValue } get { return toCropViewController.onDidCropToCircleImage } } @@ -545,6 +556,13 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { toCropViewController.setAspectRatioPreset(aspectRatio, animated: animated) } + /** + Flips image around vertical axis as if user pressed flip button in the upper right corner themself + */ + public func flipImageHorizontally() { + toCropViewController.flipImageHorizontally() + } + /** Play a custom animation of the target image zooming to its position in the crop controller while the background fades in. @@ -578,11 +596,11 @@ open class CropViewController: UIViewController, TOCropViewControllerDelegate { @param completion A block that is called once the transition animation is completed. */ public func presentAnimatedFrom(_ viewController: UIViewController, fromImage image: UIImage?, - fromView: UIView?, fromFrame: CGRect, angle: Int, toImageFrame toFrame: CGRect, + fromView: UIView?, fromFrame: CGRect, angle: Int, flipped: Bool, toImageFrame toFrame: CGRect, setup: (() -> (Void))?, completion:(() -> (Void))?) { toCropViewController.presentAnimatedFrom(viewController, fromImage: image, fromView: fromView, - fromFrame: fromFrame, angle: angle, toFrame: toFrame, + fromFrame: fromFrame, angle: angle, flippedHorizontally: flipped, toFrame: toFrame, setup: setup, completion: completion) } @@ -641,24 +659,24 @@ extension CropViewController { return } - if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropImageToRect:angle:))) { - self.onDidCropImageToRect = {[weak self] rect, angle in + if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropImageToRect:angle:flipped:))) { + self.onDidCropImageToRect = {[weak self] rect, angle, flipped in guard let strongSelf = self else { return } - delegate.cropViewController!(strongSelf, didCropImageToRect: rect, angle: angle) + delegate.cropViewController!(strongSelf, didCropImageToRect: rect, angle: angle, flipped: flipped) } } - if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropToImage:withRect:angle:))) { - self.onDidCropToRect = {[weak self] image, rect, angle in + if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropToImage:withRect:angle:flipped:))) { + self.onDidCropToRect = {[weak self] image, rect, angle, flipped in guard let strongSelf = self else { return } - delegate.cropViewController!(strongSelf, didCropToImage: image, withRect: rect, angle: angle) + delegate.cropViewController!(strongSelf, didCropToImage: image, withRect: rect, angle: angle, flipped: flipped) } } - if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropToCircularImage:withRect:angle:))) { - self.onDidCropToCircleImage = {[weak self] image, rect, angle in + if delegate.responds(to: #selector(CropViewControllerDelegate.cropViewController(_:didCropToCircularImage:withRect:angle:flipped:))) { + self.onDidCropToCircleImage = {[weak self] image, rect, angle, flipped in guard let strongSelf = self else { return } - delegate.cropViewController!(strongSelf, didCropToCircularImage: image, withRect: rect, angle: angle) + delegate.cropViewController!(strongSelf, didCropToCircularImage: image, withRect: rect, angle: angle, flipped: flipped) } } diff --git a/Swift/CropViewControllerExample/ViewController.swift b/Swift/CropViewControllerExample/ViewController.swift index 027b0a80..75514c89 100644 --- a/Swift/CropViewControllerExample/ViewController.swift +++ b/Swift/CropViewControllerExample/ViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ViewController: UIViewController, CropViewControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { +final class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { private let imageView = UIImageView() @@ -17,6 +17,9 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke private var croppedRect = CGRect.zero private var croppedAngle = 0 + private var flippedHorizontally = false + + private weak var cropController: CropViewController? func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { guard let image = (info[UIImagePickerController.InfoKey.originalImage] as? UIImage) else { return } @@ -61,7 +64,9 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke if croppingStyle == .circular { if picker.sourceType == .camera { picker.dismiss(animated: true, completion: { - self.present(cropController, animated: true, completion: nil) + self.present(cropController, animated: true, completion: { [weak self, weak cropController] in + self?.addFlipButton(to: cropController) + }) }) } else { picker.pushViewController(cropController, animated: true) @@ -69,24 +74,14 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke } else { //otherwise dismiss, and then present from the main controller picker.dismiss(animated: true, completion: { - self.present(cropController, animated: true, completion: nil) + self.present(cropController, animated: true, completion: { [weak self, weak cropController] in + self?.addFlipButton(to: cropController) + }) //self.navigationController!.pushViewController(cropController, animated: true) }) } } - public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { - self.croppedRect = cropRect - self.croppedAngle = angle - updateImageViewWithImage(image, fromCropViewController: cropViewController) - } - - public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) { - self.croppedRect = cropRect - self.croppedAngle = angle - updateImageViewWithImage(image, fromCropViewController: cropViewController) - } - public func updateImageViewWithImage(_ image: UIImage, fromCropViewController cropViewController: CropViewController) { imageView.image = image layoutImageView() @@ -179,9 +174,12 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke fromView: nil, fromFrame: viewFrame, angle: self.croppedAngle, + flipped: self.flippedHorizontally, toImageFrame: self.croppedRect, setup: { self.imageView.isHidden = true }, - completion: nil) + completion: { [weak cropViewController] in + self.addFlipButton(to: cropViewController) + }) } public override func viewDidLayoutSubviews() { @@ -224,5 +222,37 @@ class ViewController: UIViewController, CropViewControllerDelegate, UIImagePicke activityController.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem! present(activityController, animated: true, completion: nil) } + + private func addFlipButton(to controller: CropViewController?) { + guard let controller = controller else { return } + let flipButton = UIButton(type: .custom) + flipButton.setTitle("Flip", for: .normal) + flipButton.addTarget(self, action: #selector(flipImageHorizontally), for: .touchUpInside) + cropController = controller + flipButton.translatesAutoresizingMaskIntoConstraints = false + controller.view.addSubview(flipButton) + NSLayoutConstraint.activate([ + flipButton.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor, constant: -20), + flipButton.topAnchor.constraint(equalTo: controller.view.topAnchor, constant: 20) + ]) + } + + @objc private func flipImageHorizontally() { + cropController?.flipImageHorizontally() + } } +extension ViewController: CropViewControllerDelegate { + public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int, flipped: Bool) { + self.croppedRect = cropRect + self.croppedAngle = angle + self.flippedHorizontally = flipped + updateImageViewWithImage(image, fromCropViewController: cropViewController) + } + + public func cropViewController(_ cropViewController: CropViewController, didCropToCircularImage image: UIImage, withRect cropRect: CGRect, angle: Int) { + self.croppedRect = cropRect + self.croppedAngle = angle + updateImageViewWithImage(image, fromCropViewController: cropViewController) + } +}