iOS Uiimage Clip to Paths

iOS UIImage clip to paths

With one path it's very easy. Just set the path as the clipping path:

- (UIImage *)maskImage:(UIImage *)originalImage toPath:(UIBezierPath *)path {
UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, 0);
[path addClip];
[originalImage drawAtPoint:CGPointZero];
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return maskedImage;
}

If you want to use the union of multiple paths, it's harder, because Quartz doesn't have any functions that directly compute the union of two paths. One way is to fill each path one by one into a mask, and then draw the image through the mask:

- (UIImage *)maskedImage
{
CGRect rect = CGRectZero;
rect.size = self.originalImage.size;
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0); {
[[UIColor blackColor] setFill];
UIRectFill(rect);
[[UIColor whiteColor] setFill];
for (UIBezierPath *path in self.paths)
[path fill];
}
UIImage *mask = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0); {
CGContextClipToMask(UIGraphicsGetCurrentContext(), rect, mask.CGImage);
[self.originalImage drawAtPoint:CGPointZero];
}
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return maskedImage;
}

Clip UIImage to UIBezierPath (not masking)

Thanks to Rob I was able to implement a solution using the cropping(to:) method of CGImage.

This is a two step process where first I mask the image using the given path, and then crop the result using the bounds of the path.

The following is my final working source code for implementing a clipping UIBezierPath UIImage extension:

extension UIImage {

func imageByApplyingClippingBezierPath(_ path: UIBezierPath) -> UIImage {
// Mask image using path
let maskedImage = imageByApplyingMaskingBezierPath(path)

// Crop image to frame of path
let croppedImage = UIImage(cgImage: maskedImage.cgImage!.cropping(to: path.bounds)!)
return croppedImage
}

func imageByApplyingMaskingBezierPath(_ path: UIBezierPath) -> UIImage {
// Define graphic context (canvas) to paint on
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()!
context.saveGState()

// Set the clipping mask
path.addClip()
draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))

let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!

// Restore previous drawing context
context.restoreGState()
UIGraphicsEndImageContext()

return maskedImage
}

}

Saving a UIImage cropped to a CGPath

Don't throw away your layer-based approach! You can still use that when drawing in a graphics context.

Example:

func getMaskedImage(path: CGPath) -> UIImage? {
let picture = UIImage(named: "my_image")!
let imageLayer = CALayer()
imageLayer.frame = CGRect(origin: .zero, size: picture.size)
imageLayer.contents = picture.cgImage
let maskLayer = CAShapeLayer()
let maskPath = path.resized(to: CGRect(origin: .zero, size: picture.size))
maskLayer.path = maskPath
maskLayer.fillRule = .evenOdd
imageLayer.mask = maskLayer
UIGraphicsBeginImageContext(picture.size)
defer { UIGraphicsEndImageContext() }

if let context = UIGraphicsGetCurrentContext() {
imageLayer.render(in: context)

let newImage = UIGraphicsGetImageFromCurrentImageContext()

return newImage
}
return nil
}

Note that this doesn't actually resize the image. If you want the image resized, you should get the boundingBox of the resized path. Then do this instead:

// create a context as big as the bounding box
UIGraphicsBeginImageContext(boundingBox.size)
defer { UIGraphicsEndImageContext() }

if let context = UIGraphicsGetCurrentContext() {
// move the context to the top left of the path
context.translateBy(x: -boundingBox.origin.x, y: -boundingBox.origin.y)
imageLayer.render(in: context)
let newImage = UIGraphicsGetImageFromCurrentImageContext()

return newImage
}

iphone stroke path used for clipping UIImage

A path can be an object like any other - in this case, a CGPath (CGPathRef). Before using the path for anything, encapsulate it as a CGPath (in this case, probably a CGMutablePathRef, or perhaps you can call CGContextCopyPath before using the path to clip to). Now you can reuse that path.

Here's an example from one of my apps where I form a path, then stroke it, and then clip to the same path (c is the graphics context):

CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, CGRectGetMaxX(r) - radius, ins);
CGPathAddArc(path, nil,
radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true);
CGPathAddArc(path, nil,
CGRectGetMaxX(r) - radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true);
CGPathCloseSubpath(path);
CGContextAddPath(c, path);
CGContextSetLineWidth(c, 2);
CGContextStrokePath(c);
CGContextAddPath(c, path);
CGContextClip(c);
CGPathRelease(path);

Another possibility is to use UIBezierPath - a full-fledged Objective-C object, instead of CGContext functions. It encapsulates a CGPath and you can reuse that path - clip to it, then stroke it. Or the other way round.

Clipping a UIImage to a custom UIBezierPath while preserving the image quality

You are getting an incorrect crop because the UIImage is scaled to fit inside the UIImageView. Basically this means you have to translate the UIBezierPath coordinates to the correct coordinates within the UIImage. The easiest way to do this is to use a UIImageView category which will convert the points from one view (in this case the UIBezierPath, even though it's not really a view) to the correct points within the UIImageView.
You can see an example of such a category here. More specifically you will need to use the convertPointFromView: method within that category to convert each point in your UIBezierPath.
(Sorry for not writing the complete code, I'm typing on my phone)



Related Topics



Leave a reply



Submit