ios - Como cortar um UIImageView para uma nova UIImage no modo 'preenchimento de aspecto'?



objective-c iphone (1)

Vamos dividir o problema em duas partes:

  1. Dado o tamanho de um UIImageView e o tamanho de seu UIImage, se o modo de conteúdo do UIImageView for Aspect Fill, qual é a parte do UIImage que se encaixa no UIImageView? Precisamos, com efeito, cortar a imagem original para corresponder ao que o UIImageView está realmente exibindo.

  2. Dado um retângulo arbitrário no UIImageView, a que parte da imagem cortada (derivada na parte 1) ela corresponde?

A primeira parte é a parte interessante, então vamos tentar. (A segunda parte será trivial.)

Aqui está a imagem original que vou usar:

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

Essa imagem é 1000x611. Aqui está o que parece reduzido (mas lembre-se de que vou usar a imagem original ):

Minha exibição de imagem, no entanto, será 139 x 182 e está definida como Preenchimento de aspecto. Quando exibe a imagem, fica assim:

O problema que queremos resolver é: que parte da imagem original está sendo exibida na minha visualização de imagem, se ela estiver definida como Preenchimento de aspecto?

Aqui vamos nós. Suponha que iv é a visualização da imagem:

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)

Portanto, agora resolvemos o problema: croppedImrect é a região da imagem original exibida na visualização da imagem. Vamos continuar usando nosso conhecimento, cortando a imagem para uma nova imagem que corresponda ao que é mostrado na visualização da imagem:

let r = UIGraphicsImageRenderer(size:croppedImsize)
let croppedIm = r.image { _ in
    iv.image!.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
}

O resultado é esta imagem (ignore a borda cinza):

Mas eis que esta é a resposta correta! Extraí da imagem original exatamente a região retratada no interior da visualização da imagem.

Então agora você tem todas as informações necessárias. croppedIm é a UIImage realmente exibida na área cortada da visualização da imagem. scale é a escala entre a visualização da imagem e essa imagem. Portanto, você pode resolver facilmente o problema proposto originalmente! Dado qualquer retângulo imposto à vista da imagem, nas coordenadas dos limites da vista da imagem, basta aplicar a escala (ou seja, dividir todos os quatro de seus atributos por scale ) - e agora você tem o mesmo retângulo que uma parte do croppedIm .

(Observe que realmente não precisamos recortar a imagem original para croppedIm ; era suficiente, na realidade, saber como realizar essa recorte. As informações importantes são a scale junto com a origin do croppedImRect ; dadas essas informações, você pode pegar o retângulo imposto à vista da imagem, redimensioná-lo e deslocá-lo para obter o retângulo desejado da imagem original.)

EDIT Adicionei um pequeno screencast apenas para mostrar que minha abordagem funciona como uma prova de conceito:

EDIT Também criou um projeto de exemplo para download aqui:

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

Mas note que não posso garantir que o URL dure para sempre. Leia a discussão acima para entender a abordagem usada.

Estou tentando cortar uma sub-imagem de uma exibição de imagem usando uma sobreposição UIView que pode ser posicionada em qualquer lugar do UIImageView . Estou emprestando uma solução de uma postagem semelhante sobre como resolver isso quando o modo de conteúdo do UIImageView é 'Aspect Fit'. Essa solução proposta é:

  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)
    }

O problema é que o uso dessa solução quando a visualização da imagem é preenchida pelo aspecto faz com que o segmento recortado não se alinhe exatamente com o local em que a UIView sobreposição foi posicionada. Não tenho muita certeza de como adaptar esse código para acomodar o Aspect Fill ou reposicionar minha sobreposição UIView para que ela se alinhe em 1: 1 com o segmento que estou tentando cortar.

UPDATE Resolvido usando a resposta de Matt abaixo

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")
    }
}

O resultado final





uiimageview