0

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.

SwiftArtery
  • 296
  • 1
  • 10
  • Does this answer your question? [layer.renderInContext doesn't take layer.mask into account?](https://stackoverflow.com/questions/4896296/layer-renderincontext-doesnt-take-layer-mask-into-account) – emptyhua Oct 09 '21 at 13:01

1 Answers1

2

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
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • That's done it! Thank you so much :) Both examples work so will just see which is better for this scenario. Thanks again! – SwiftArtery Oct 09 '21 at 13:26
  • 1
    @chumps52 It depends on whether you want a smaller image whenever possible or not. – Sweeper Oct 09 '21 at 13:27
  • I don't suppose you know how to set it so the area that is currently masked is actually removed/made transparent? In the `UIImageView` preview that's the case, but when saving it ^ it is white. – SwiftArtery Oct 09 '21 at 13:45
  • 1
    It's not white for me... Did you happen to save it as a jpg, which doesn't support transparency? @chumps52 – Sweeper Oct 09 '21 at 13:54
  • Ah - I was using `UIImageWriteToSavedPhotosAlbum(saveImage, nil, nil, nil)` to save to the user's album which appears to be jpeg I guess? That gives the white mask. Saving to a file using `saveImage.pngData()` works though. – SwiftArtery Oct 09 '21 at 14:01
  • These approaches have issues. You risk opening an image context and never closing it. And drawing a CGImage loses scale/resolution information. – matt Oct 09 '21 at 14:15
  • Also you're not taking into account the image view content mode. – matt Oct 09 '21 at 14:18
  • @matt Okay, I agree with your first comment, but as far as I understand the question, OP here is trying to crop an _image_. I don't see how any image _views_ are relevant... – Sweeper Oct 09 '21 at 14:26
  • "I have got this working when displayed in a UIImageView preview as follows" So there is a path that crops the image as displayed in an image view in aspect fit content mode. But that same path will not necessarily be in the same place or size when "projected" onto the original _image_ that is being displayed in the image view. This is the problem that my essay at https://stackoverflow.com/questions/43720720/how-to-crop-a-uiimageview-to-a-new-uiimage-in-aspect-fill-mode/43720791#43720791 discusses. Except that my example is "just" a rectangular crop. – matt Oct 09 '21 at 14:53
  • @matt The OP also says "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`". Doesn't that sentence invalidate your interpretation? – Sweeper Oct 09 '21 at 15:01
  • I don't think so; I think it confirms it. But the OP isn't complaining about the results, so what do I know? – matt Oct 09 '21 at 15:21