Reputation: 1565
I think I may have an odd request, however hopefully someone can help. I am using the well known UIScrollView
+ UIImageView
to zoom into and out of an image, as well as pan. This works fine and dandy, but the current project we have needs to be able to crop the image, but also include the black bars on the sides if the image is smaller than the crop rectangle. See the images below.
We wish to capture everything inside of the blue box, including the white (which will be black, since opaque is set to YES
).
This works great for images that are completely zoomed out (The white is just the UIImageView's extra space).
However the problem arises when we try to zoom into the image, and capture only that portion, plus the empty space.
This results in the following image
The problem we are seeing is we need to be able to create an image that is exactly what is in the Crop Rect, regardless if there is part of the image there or not. The other problem is we wish to have the ability to dynamically change the output resolution. The aspect ratio is 16:9, and for this example kMaxWidth = 1136
and kMaxHeight = 639
, however in the future we may want to request a larger or smaller 16:9 resolution.
Below is the function I have so far:
- (UIImage *)createCroppedImageFromImage:(UIImage *)image {
CGSize newRect = CGSizeMake(kMaxWidth, kMaxHeight);
UIGraphicsBeginImageContextWithOptions(newRect, YES, 0.0);
// 0 is the edge of the screen, to help with zooming
CGFloat xDisplacement = ((abs(0 - imageView.frame.origin.x) * kMaxWidth) / (self.cropSize.width / self.scrollView.zoomScale) / self.scrollView.zoomScale);
CGFloat yDisplacement = ((abs(self.cropImageView.frame.origin.y - imageView.frame.origin.y) * kMaxHeight) / (self.cropSize.height / self.scrollView.zoomScale) / self.scrollView.zoomScale);
CGFloat newImageWidth = (self.image.size.width * kMaxWidth) / (self.cropSize.width / self.scrollView.zoomScale);
CGFloat newImageHeight = (self.image.size.height * kMaxHeight) / (self.cropSize.height / self.scrollView.zoomScale);
[image drawInRect:CGRectMake(xDisplacement, 0, newImageWidth, newImageHeight)];
UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return croppedImage;
}
Any help would be greatly appreciated.
Upvotes: 1
Views: 4528
Reputation: 78
I have made my own approach.
First of all you need to understand that zooming affects your crop result and image ratio with imageView also matters.
MaskView - is a view with a mask, which displays an area which need to crop. It is added in main view and on top of the scrollView. You can see an area which you want to crop on the image below.
Mask creation if you need it(you need to call this method in viewDidAppear after all sizes are known):
func setupMask() {
maskHeight = imageView.frame.height
let maskPath = UIBezierPath(ovalIn: CGRect(x: imageView.frame.width / 2 - maskHeight / 2,
y: imageView.frame.height / 2 - maskHeight / 2,
width: maskHeight,
height: maskHeight))
let fullPath = UIBezierPath(rect: view.bounds)
fullPath.append(maskPath)
fullPath.usesEvenOddFillRule = true
maskLayer.path = fullPath.cgPath
maskLayer.fillRule = .evenOdd
maskLayer.fillColor = UIColor.black.cgColor
maskLayer.opacity = 0.7
self.maskView.layer.addSublayer(maskLayer)
self.view.addSubview(maskView)
maskView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
maskView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0),
maskView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0),
maskView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
maskView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0),
])
}
And here is a cropImage function which makes a magic
func cropImage() -> UIImage? {
guard let originalImage = imageView.image else { return nil }
let scrollViewZoomScale = scrollView.zoomScale
//image size in imageView. The image may not fill image view completely, it may fill only part especially when the content mode set to aspect fit. And for the correct calculations you need to know sizes of the image in imageView.
let aspectFitSize = imageSizeInImageView(image: originalImage, imageView: imageView)
//We have a maskView added to view on top of the scrollView. It is always in the center and we need to convert its center point to imageView coordinate system
let maskCenterInImageView = view.convert(maskView.center, to: imageView)
//Find offset of image in imageView. As I mentioned earlier, image may not completely fill imageView. Here we calculate the offset for left and top.
let imageOffsetX = (imageView.bounds.width - aspectFitSize.width) / 2
let imageOffsetY = (imageView.bounds.height - aspectFitSize.height) / 2
//apply the offset to know the center point
let maskCenterInImage = CGPoint(x: maskCenterInImageView.x - imageOffsetX,
y: maskCenterInImageView.y - imageOffsetY)
//find rectagle of the mask area zoomed by scrollView. Here we need to know coordinates, so subtract maskHeight / 2 to know x point. And also apply zoom by scrollView
let cropRect = CGRect(x: maskCenterInImage.x - (maskHeight / 2) / scrollViewZoomScale,
y: maskCenterInImage.y - (maskHeight / 2) / scrollViewZoomScale,
width: maskHeight / scrollViewZoomScale ,
height: maskHeight / scrollViewZoomScale )
//ratio to original image with image in imageView
let imageRatio = originalImage.size.width / aspectFitSize.width
//convert coordinates to the real image with imageRatio
let scaledCropRect = CGRect(
x: cropRect.origin.x * imageRatio,
y: cropRect.origin.y * imageRatio,
width: cropRect.width * imageRatio,
height: cropRect.height * imageRatio
)
guard let croppedCGImage = originalImage.cgImage?.cropping(to: scaledCropRect) else { return nil }
let croppedImage = UIImage(cgImage: croppedCGImage)
return croppedImage
}
Function to return image size in image view
///Returns size of the image in imageView
func imageSizeInImageView(image: UIImage, imageView: UIImageView) -> CGSize {
var aspectFitSize = CGSize(width: imageView.bounds.size.width, height: imageView.bounds.size.height)
let mW = imageView.bounds.size.width / image.size.width
let mH = imageView.bounds.size.height / image.size.height
if mH < mW {
aspectFitSize.width = mH * image.size.width
} else if mH > mW {
aspectFitSize.height = mW * image.size.height
}
return aspectFitSize
}
Upvotes: 0
Reputation: 2165
I think the translated rect for the image view isn't calculated properly. Since UIImageView is the subview inside the UIScrollView, you should be able to calculate the visible rect by calling [scrollView convertRect:scrollView.bounds toView:imageView];
. That will be the visible rect of your image view. All you need to now is crop it.
-(UIImage*)cropImage:(UIImage*)img inRect:(CGRect)rect{
CGImageRef cropped = CGImageCreateWithImageInRect(img.CGImage, rect);
UIImage *image = [UIImage imageWithCGImage:cropped];
CGImageRelease(cropped);
return image;
}
Edit: Yeah... I forgot to mention that cropping should be done in (0,1) coordinate space. I've modified the crop function for you, so it crops the image based on all parameters you provided, UIImageView inside UIScrollView and an image.
-(UIImage*)cropImage:(UIImage*)image inImageView:(UIImageView*)imageView scrollView:(UIScrollView*)scrollView{
// get visible rect from image scrollview
CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:imageView];
UIImage* rCroppedImage;
CALayer* maskLayer= [[CALayer alloc] init];
maskLayer.contents= (id)image.CGImage;
maskLayer.frame= CGRectMake(0, 0, visibleRect.size.width, visibleRect.size.height);
CGRect rect= CGRectMake(visibleRect.origin.x / image.size.width,
visibleRect.origin.y / image.size.height,
visibleRect.size.width / image.size.width,
visibleRect.size.height / image.size.height);
maskLayer.contentsRect= rect;
UIGraphicsBeginImageContext(visibleRect.size);
CGContextRef context= UIGraphicsGetCurrentContext();
[maskLayer renderInContext:context];
rCroppedImage= UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return rCroppedImage;
}
Upvotes: 0
Reputation: 1565
I ended up just taking a screenshot, and cropping that. It seems to work well enough.
- (UIImage *)cropImage {
CGRect cropRect = self.cropOverlay.cropRect;
UIGraphicsBeginImageContext(self.view.frame.size);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *fullScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRef croppedImage = CGImageCreateWithImageInRect(fullScreenshot.CGImage, cropRect);
UIImage *crop = [[UIImage imageWithCGImage:croppedImage] resizedImage:self.outputSize interpolationQuality:kCGInterpolationHigh];
CGImageRelease(croppedImage);
return crop;
}
If using iOS 7, you would use drawViewHierarchyInRect:afterScreenUpdates:
, instead of renderInContext:
Upvotes: 3