Alex Muller
Alex Muller

Reputation: 1565

Crop a Portion of UIImage from Larger UIImage, and include non-image parts

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). enter image description here

However the problem arises when we try to zoom into the image, and capture only that portion, plus the empty space. enter image description here

This results in the following image enter image description here

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

Answers (3)

ATOM
ATOM

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. Example of a mask view 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

Mercurial
Mercurial

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

Alex Muller
Alex Muller

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

Related Questions