如何在“等比缩放填充”模式下裁剪UIImageView并生成新的UIImage?

14

我正在尝试使用覆盖UIView裁剪UIImageView中可以放置在任何位置的子图像。我借鉴了一个类似帖子中解决此问题的解决方案,当UIImageView内容模式为“Aspect Fit”时如何解决。那个提出的解决方案是:

  func computeCropRect(for sourceFrame : CGRect) -> CGRect {

        let widthScale = bounds.size.width / image!.size.width
        let heightScale = bounds.size.height / image!.size.height

        var x : CGFloat = 0
        var y : CGFloat = 0
        var width : CGFloat = 0
        var height : CGFloat = 0
        var offSet : CGFloat = 0

        if widthScale < heightScale {
            offSet = (bounds.size.height - (image!.size.height * widthScale))/2
            x = sourceFrame.origin.x / widthScale
            y = (sourceFrame.origin.y - offSet) / widthScale
            width = sourceFrame.size.width / widthScale
            height = sourceFrame.size.height / widthScale
        } else {
            offSet = (bounds.size.width - (image!.size.width * heightScale))/2
            x = (sourceFrame.origin.x - offSet) / heightScale
            y = sourceFrame.origin.y / heightScale
            width = sourceFrame.size.width / heightScale
            height = sourceFrame.size.height / heightScale
        }

        return CGRect(x: x, y: y, width: width, height: height)
    }

问题在于,当使用此解决方案时,如果图像视图采用了 aspect fill 模式,则裁剪的部分与覆盖的 UIView 的位置不完全对齐。我不确定如何调整此代码以适应 aspect fill 模式,或重新定位我的覆盖 UIView 使其与我要裁剪的部分一一对齐。

更新:使用Matt下面的答案已经解决了问题。

class ViewController: UIViewController {

    @IBOutlet weak var catImageView: UIImageView!
    private var cropView : CropView!

    override func viewDidLoad() {
        super.viewDidLoad()

        cropView = CropView(frame: CGRect(x: 0, y: 0, width: 45, height: 45))

        catImageView.image = UIImage(named: "cat")
        catImageView.clipsToBounds = true

        catImageView.layer.borderColor = UIColor.purple.cgColor
        catImageView.layer.borderWidth = 2.0
        catImageView.backgroundColor = UIColor.yellow

        catImageView.addSubview(cropView)

        let imageSize = catImageView.image!.size
        let imageViewSize = catImageView.bounds.size

        var scale : CGFloat = imageViewSize.width / imageSize.width

        if imageSize.height * scale < imageViewSize.height {
            scale = imageViewSize.height / imageSize.height
        }

        let croppedImageSize = CGSize(width: imageViewSize.width/scale, height: imageViewSize.height/scale)

        let croppedImrect =
            CGRect(origin: CGPoint(x: (imageSize.width-croppedImageSize.width)/2.0,
                                   y: (imageSize.height-croppedImageSize.height)/2.0),
                   size: croppedImageSize)

        let renderer = UIGraphicsImageRenderer(size:croppedImageSize)

        let _ = renderer.image { _ in
            catImageView.image!.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
        }
    }


    @IBAction func performCrop(_ sender: Any) {
        let cropFrame = catImageView.computeCropRect(for: cropView.frame)
        if let imageRef = catImageView.image?.cgImage?.cropping(to: cropFrame) {
            catImageView.image = UIImage(cgImage: imageRef)
        }
    }

    @IBAction func resetCrop(_ sender: Any) {
        catImageView.image = UIImage(named: "cat")
    }
}

最终结果 enter image description here


但是你的新材料展示了一个屏幕截图,原始图像没有受到aspectFillaspectFit的影响;整个图像都被显示出来了!我认为你的图像视图没有按照你想象的那样配置。 - matt
我要快速更新我的实现和截图,以便更加清晰明了。 - zic10
那么,你在完全错误的时间创建了 aspectCroppedImage - matt
这段代码应该在什么时候被调用以计算它的结果? - zic10
我的实现中有一些错误导致你的解决方案出现了问题。我发布了一个gif,看起来它正在按预期工作。感谢你的所有帮助! - zic10
显示剩余2条评论
2个回答

24

让我们将问题分为两部分:

  1. 如果 UIImageView 的 content mode 是 Aspect Fill,给定 UIImageView 和 UIImage 的尺寸,那么 UIImage 中适应 UIImageView 的哪一部分是什么?实际上,我们需要裁剪原始图像以匹配 UIImageView 实际显示的内容。

  2. 在 UIImageView 中,如果给定任意的矩形范围,它对应于裁剪后的图像(在第1部分中得出)的哪一部分?

第一部分是有趣的部分,所以我们来试试看。(第二部分将会变得微不足道。)

这是我要使用的原始图像:

https://static1.squarespace.com/static/54e8ba93e4b07c3f655b452e/t/56c2a04520c64707756f4267/1455596221531/

那张图片的尺寸是1000x611。以下是缩小后的样子(但请记住,我将一直使用原始图片):

enter image description here

我的图像视图大小为139x182,设置为Aspect Fill。当它显示图像时,看起来像这样:

enter image description here

我们要解决的问题是:如果我的图像视图设置为Aspect Fill,那么显示在我的图像视图中的是原始图片的哪一部分?
假设iv是图像视图:
let imsize = iv.image!.size
let ivsize = iv.bounds.size

var scale : CGFloat = ivsize.width / imsize.width
if imsize.height * scale < ivsize.height {
    scale = ivsize.height / imsize.height
}

let croppedImsize = CGSize(width:ivsize.width/scale, height:ivsize.height/scale)
let croppedImrect =
    CGRect(origin: CGPoint(x: (imsize.width-croppedImsize.width)/2.0,
                           y: (imsize.height-croppedImsize.height)/2.0),
           size: croppedImsize)

现在,我们已经解决了问题: croppedImrect 是在图像视图中显示的原始图像区域。让我们继续利用我们的知识,通过实际裁剪图像来创建一个新的图像,与图像视图中显示的内容相匹配:
let r = UIGraphicsImageRenderer(size:croppedImsize)
let croppedIm = r.image { _ in
    iv.image!.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
}

结果是这张图片(忽略灰色边框):

enter image description here

但是,惊人的是,这就是正确答案!我从原始图像中提取了确切地在图像视图内部描绘的区域。
所以现在你拥有了所有需要的信息。 croppedIm 是实际显示在图像视图中剪裁区域的UIImage。 scale 是图像视图和该图像之间的比例尺。因此,您可以轻松解决最初提出的问题!给定任何在图像视图上施加的矩形,在图像视图的边界坐标中,只需应用比例(即将其四个属性全部除以scale)-现在您已经拥有了与croppedIm的一部分相同的矩形。
(请注意,我们没有真正地需要裁剪原始图像以获得croppedIm;实际上,知道如何执行该裁剪就足够了。重要的信息是scale以及croppedImRectorigin;给定该信息,您可以将施加于图像视图上的矩形进行缩放,并进行偏移以获取原始图像的所需矩形。)

编辑 我添加了一个小的屏幕录像以证明我的方法是可行的:

enter image description here

编辑:此外,我在这里创建了一个可下载的示例项目:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/39cc800d18aa484d17c26ffcbab8bbe51c614573/bk2ch02p058cropImageView/Cropper/ViewController.swift

但请注意,我不能保证该URL永久存在,因此请阅读上面的讨论以了解所使用的方法。

1
我理解你的帖子,但是“仅应用比例,现在您就拥有了与croppedIm的一部分相同的矩形”的意思可以详细说明吗?我的裁剪视图大小和位置是动态的,即使使用croppedIm并且使用我上面提供的功能进行裁剪,裁剪仍然不正确,我不确定原因在哪里。 - zic10
我添加了一个小的屏幕录像来展示我的方法正确地将图像裁剪到指定的矩形。 - matt
我更新了我的帖子,提供了可行的解决方案,并附上了演示功能的gif。 - zic10
@matt,只是好奇为什么您必须在绘制命令上取反值。 - Juan Boero
@JuanBoero 尝试不否定它们并看看。 - matt
显示剩余8条评论

1
马特完美地回答了这个问题。我正在创建一个全屏相机,需要使最终输出与全屏预览匹配。在此提供马特在Swift 5中整体答案的紧凑扩展,以便其他人轻松使用。建议阅读马特的答案,因为它非常好地解释了事情。
extension UIImage {
    func cropToRect(rect: CGRect) -> UIImage? {
        var scale = rect.width / self.size.width
        scale = self.size.height * scale < rect.height ? rect.height/self.size.height : scale

        let croppedImsize = CGSize(width:rect.width/scale, height:rect.height/scale)
        let croppedImrect = CGRect(origin: CGPoint(x: (self.size.width-croppedImsize.width)/2.0,
                                                   y: (self.size.height-croppedImsize.height)/2.0),
                                                   size: croppedImsize)
        UIGraphicsBeginImageContextWithOptions(croppedImsize, true, 0)
        self.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
        let croppedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return croppedImage
    }
}

似乎根本不起作用 - 无论您传递其他原点或其他大小,我始终得到相同的矩形。结果始终如一。 - Vyachaslav Gerchicov
我很愿意回答你的问题,但是你选择对一个可行的解决方案进行了负面评价,因为它可能不适用于你的特定情况或存在错误。祝你好运。 - C6Silver
是的,也许我的代码包含错误/漏洞,但无论如何,经过确认的答案可行,而你的不行。 - Vyachaslav Gerchicov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接