如何在“方面填充”模式下将 UIImageView 裁剪为新的 UIImage?

Posted

技术标签:

【中文标题】如何在“方面填充”模式下将 UIImageView 裁剪为新的 UIImage?【英文标题】:How to crop a UIImageView to a new UIImage in 'aspect fill' mode? 【发布时间】:2017-05-01 14:26:04 【问题描述】:

我正在尝试使用覆盖层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)
    

问题在于,当图像视图为纵横填充时使用此解决方案会导致裁剪的片段与覆盖UIView 的位置不完全对齐。我不太确定如何调整此代码以适应 Aspect Fill 或重新定位我的叠加层UIView,以便它与我要裁剪的片段 1:1 对齐。

UPDATE 使用下面的 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")
    

最终结果

【问题讨论】:

但是你的新素材显示了一个屏幕截图,其中原始图像不受aspectFillaspectFit的影响;正在显示整个图像!我认为您的图像视图的配置方式与您想象的不一样。 我仔细检查了两个图像视图都设置为aspectFill。可能是因为我直接将aspectCroppedImage 分配给destinationImageView 为了清晰起见,我将快速更新我的实现和屏幕截图。 那么您在完全错误的时间创建aspectCroppedImage 实际应该何时调用计算它的代码? 【参考方案1】:

让我们把问题分成两部分:

    给定 UIImageView 的大小及其 UIImage 的大小,如果 UIImageView 的内容模式是 Aspect Fill, UIImage 中适合 UIImageView 的部分是什么?实际上,我们需要裁剪原始图像以匹配 UIImageView 实际显示的内容。

    给定 UIImageView 中的任意矩形,它对应于裁剪图像的哪一部分(源自第 1 部分)?

第一部分是有趣的部分,所以让我们尝试一下。 (然后第二部分将变得微不足道。)

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

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

该图像为 1000x611。这是缩小后的样子(但请记住,我将始终使用原始图像):

但是,我的图像视图将是 139x182,并设置为 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))

结果是这个图像(忽略灰色边框):

但是你瞧,这是正确的答案!我已经从原始图像中准确地提取了图像视图内部描绘的区域。

所以现在您拥有所需的所有信息。 croppedIm 是实际显示在图像视图的剪切区域中的 UIImage。 scale 是图像视图和该图像之间的比例。因此,您可以轻松解决您最初提出的问题!给定图像视图上的任何矩形,在图像视图的边界坐标中,您只需应用比例(即,将其所有四个属性除以scale) - 现在您拥有与croppedIm 的一部分相同的矩形。

(请注意,我们真的不需要裁剪原始图像以获得croppedIm;实际上,知道如何执行该操作就足够了裁剪。重要的信息是scale 以及croppedImRectorigin;根据这些信息,您可以将矩形施加在图像视图上,对其进行缩放、并将其偏移到获取原始图像的所需矩形。)


编辑我添加了一个小截屏视频,只是为了表明我的方法可以作为概念证明:

EDIT还在此处创建了一个可下载的示例项目:

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

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

【讨论】:

我了解您发布的内容,但您能否详细说明“只需应用比例,现在您拥有与croppedIm 的一部分相同的矩形”是什么意思。我的裁剪视图在大小和位置上是动态的,即使使用croppedIm 并使用我在裁剪上方提供的功能仍然关闭,我不太清楚为什么。 如果您的代码适用于切面匹配,那么如果您替换 croppedIm 并使用切面匹配,它将起作用。那么为什么不这样做呢?这就是我将其简化为以前解决的问题的意思。 是的,我明白你在说什么。我将使用代码更新我的原始问题,包括您的上下文解决方案。 我添加了一个小截屏视频,以表明我的方法可以正确地将图像实际裁剪为指定的矩形。 用可行的解决方案和一个演示功能的 gif 更新了我的帖子。【参考方案2】:

马特完美地回答了这个问题。我正在创建一个全屏相机,并且需要使最终输出与全屏预览相匹配。在这里提供了 Matt 在 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
    

【讨论】:

它似乎根本不起作用 - 我总是得到相同的矩形。无论您传递其他来源或其他大小 - 结果始终相同 我希望回答您的问题,但您不只是提出问题,而是选择否决可行的解决方案,因为它可能不适合您的特定用例或错误。祝你好运。 是的,也许我的代码包含错误/错误,但无论如何确认的答案有效,您的答案无效。

以上是关于如何在“方面填充”模式下将 UIImageView 裁剪为新的 UIImage?的主要内容,如果未能解决你的问题,请参考以下文章

swift UIImage使用UIImageView调整方面填充和裁剪

如何将边框添加到内容模式设置为方面填充的 UIImage

与 Aspect Fill 类似的行为,但使用图像的顶部

如何在没有底座的情况下将 android 置于汽车模式?

如何在不丢失阴影饱和度的情况下将带有阴影的 UIBezierpath 转换为 UIImage

如何在编辑模式下将子视图添加到 UITableViewCell 并调整其大小?