Kev Wats
Kev Wats

Reputation: 1296

Take screenshot of UIImageView with AspectFill on ios

I am simply trying to capture a UIImage of a UIImageView with the contentMode set to aspectFill, but sometimes it is not working. I need it to always be of size 375 x 667 as well and perhaps the problem is associated with this but I from my testing I haven't been able to fix it :/

Here is the code used:

To Get Image:

extension UIView {

    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: 0, y: 0, width: 375, height: 667))
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }

}

Usage:

//ImageView setup stuff
imgViewForVideo.image = thumbnailImage
imgViewForVideo.contentMode = .scaleAspectFill
imgViewForVideo.isHidden = false
                
let newImage = imgViewForVideo.asImage() //usage
    
UIImageWriteToSavedPhotosAlbum(newImage, self, #selector(media(_:didFinishSavingWithError:contextInfo:)), nil) //saving it to phone for testing

And here are 2 examples of what I mean: (the images should be aspect fill and fill teh entire 375 x 667 screen no matter the original UIImage size...)

Correctly aspect filled and screenshot:

enter image description here

This is an example of a mess-up: (NOTE: the black border on the left is not part of the problem that was a mistake screenshotting from my computer.. however it helps to show the white part of the screen... which is one of the problems I encounter... other than the image being way too zoomed in sometimes..)

enter image description here

Upvotes: 1

Views: 766

Answers (1)

DonMag
DonMag

Reputation: 77462

With your current extension, you are saying:

"Render the view at its current size in a 375 x 667 image"

So, if your imgViewForVideo is 80 x 142 (such as showing a "thumbnail" at about the same aspect ratio), you're doing this:

enter image description here

What you want to do is:

get a UIImage of the view at its current size and scale it to 375 x 667

You can do that either by setting the frame of your imgViewForVideo to 375 x 667, or, to use the image view as-is, use this extension:

extension UIView {
    
    // this method will work, but uses multiple image scaling operations
    // resulting in loss of image quality
    
    func resizedImage(_ size: CGSize, useScreenScale: Bool? = true) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        if useScreenScale == false {
            format.scale = 1
        }
        // use bounds of self
        var renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
        let img = renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
        // use target size
        renderer = UIGraphicsImageRenderer(size: size, format: format)
        return renderer.image { (context) in
            img.draw(in: CGRect(origin: .zero, size: size))
        }
    }
    
}

and call it with:

    let targetSZ = CGSize(width: 375, height: 667)
    let newImage = imgViewForVideo.resizedImage(targetSZ, useScreenScale: false)

Note that method ends up scaling the image multiple times, resulting in loss of quality.

A better approach would be to use the original image and scale and crop it to your target size.

Take a look at this extension:

extension UIImage {
    
    // scales and clips original image
    // optionally preserving aspect ratio
    
    func scaleTo(size targetSize: CGSize, mode: UIView.ContentMode? = .scaleToFill, useScreenScale: Bool? = true) -> UIImage {
        // make sure a valid scale mode was requested
        //  if not, set it to scaleToFill
        var sMode: UIView.ContentMode = mode ?? .scaleToFill
        let validModes: [UIView.ContentMode] = [.scaleToFill, .scaleAspectFit, .scaleAspectFill]
        if !validModes.contains(sMode) {
            print("Invalid contentMode requested - using scaleToFill")
            sMode = .scaleToFill
        }
        
        var scaledImageSize = targetSize
        
        // if scaleToFill, don't maintain aspect ratio
        if mode != .scaleToFill {
            // Determine the scale factor that preserves aspect ratio
            let widthRatio = targetSize.width / size.width
            let heightRatio = targetSize.height / size.height
            
            // scaleAspectFit
            var scaleFactor = min(widthRatio, heightRatio)
            if mode == .scaleAspectFill {
                // scaleAspectFill
                scaleFactor = max(widthRatio, heightRatio)
            }
            
            // Compute the new image size that preserves aspect ratio
            scaledImageSize = CGSize(
                width: size.width * scaleFactor,
                height: size.height * scaleFactor
            )
        }
        
        // UIGraphicsImageRenderer uses screen scale, so...
        //  if targetSize is 100x100
        //      on an iPhone 8, for example, screen scale is 2
        //          renderer will produce a 750 x 1334 image
        //      on an iPhone 11 Pro, for example, screen scale is 3
        //          renderer will produce a 1125 x 2001 image
        //
        // if we want a pixel-exact image, set format.scale = 1
        let format = UIGraphicsImageRendererFormat()
        if useScreenScale == false {
            format.scale = 1
        }
        
        let renderer = UIGraphicsImageRenderer(
            size: targetSize,
            format: format
        )
        var origin = CGPoint.zero
        if mode != .scaleToFill {
            origin.x = (targetSize.width - scaledImageSize.width) * 0.5
            origin.y = (targetSize.height - scaledImageSize.height) * 0.5
        }
        let scaledImage = renderer.image { _ in
            self.draw(in: CGRect(
                origin: origin,
                size: scaledImageSize
            ))
        }
        
        return scaledImage
    }
    
}

Instead of calling a "convert to image" function on your image view, call scaleTo(...) directly on the image itself:

    // make sure the image view has a valid image to begin with
    guard let img = imgViewForVideo.image else {
        print("imgViewForVideo has no image !!!")
        return
    }
    
    let targetSZ = CGSize(width: 375, height: 667)
    let newImage = img.scaleTo(size: targetSZ, mode: .scaleAspectFill, useScreenScale: false)

Here's an example of a 2400 x 1500 image, displayed in-app in a 80 x 142 .scaleAspectFill image view, saved out to 375 x 667, using the UIView extension:

enter image description here

This is the same example 2400 x 1500 image, displayed in-app in a 80 x 142 .scaleAspectFill image view, saved out to 375 x 667, using the UIImage extension:

enter image description here

These used this original 2400 x 1500 image:

enter image description here

I put an example app (that I used to generate these images) here: https://github.com/DonMag/ImageSaveExample

Upvotes: 1

Related Questions