Reputation: 1296
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:
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..)
Upvotes: 1
Views: 766
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:
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:
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:
These used this original 2400 x 1500 image:
I put an example app (that I used to generate these images) here: https://github.com/DonMag/ImageSaveExample
Upvotes: 1