Home > front end >  Saving a UIImage cropped to a CGPath
Saving a UIImage cropped to a CGPath

Time:10-10

I am trying to mask a UIImage and then save the masked image. So far, I have got this working when displayed in a UIImageView preview as follows:

let maskLayer = CAShapeLayer()
let maskPath = shape.cgPath
maskLayer.path = maskPath.resized(to: imageView.frame)
maskLayer.fillRule = .evenOdd
imageView.layer.mask = maskLayer

let picture = UIImage(named: "1")!
imageView.contentMode = .scaleAspectFit
imageView.image = picture

where shape is a UIBezierPath().

The resized function is:

extension CGPath {
    func resized(to rect: CGRect) -> CGPath {
        let boundingBox = self.boundingBox
        let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
        let viewAspectRatio = rect.width / rect.height
        let scaleFactor = boundingBoxAspectRatio > viewAspectRatio ?
            rect.width / boundingBox.width :
            rect.height / boundingBox.height

        let scaledSize = boundingBox.size.applying(CGAffineTransform(scaleX: scaleFactor, y: scaleFactor))
        let centerOffset = CGSize(
            width: (rect.width - scaledSize.width) / (scaleFactor * 2),
            height: (rect.height - scaledSize.height) / (scaleFactor * 2)
        )

        var transform = CGAffineTransform.identity
            .scaledBy(x: scaleFactor, y: scaleFactor)
            .translatedBy(x: -boundingBox.minX   centerOffset.width, y: -boundingBox.minY   centerOffset.height)

        return copy(using: &transform)!
    }
}

So this works in terms of previewing the outcome I'd like. I'd now like to save this modified UIImage to the user's photo album, in it's original size (so basically generate it again but don't resize the image to fit a UIImageView - keep it as-is and apply the mask over it).

I have tried this, but it just saves the original image - no path/mask applied:

func getMaskedImage(path: CGPath) {
    let picture = UIImage(named: "1")!
    UIGraphicsBeginImageContext(picture.size)

    if let context = UIGraphicsGetCurrentContext() {
        let pathNew = path.resized(to: CGRect(x: 0, y: 0, width: picture.size.width, height: picture.size.height))
        context.addPath(pathNew)
        context.clip()

        picture.draw(in: CGRect(x: 0.0, y: 0.0, width: picture.size.width, height: picture.size.height))

        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        UIImageWriteToSavedPhotosAlbum(newImage!, nil, nil, nil)
    }
}

What am I doing wrong? Thanks.

CodePudding user response:

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
}
  • Related