¿Por qué cgImage? .cropping rotando la image?

He escrito un UICropperViewController y funciona perfectamente para imágenes en modo horizontal. Las imágenes en modo vertical tienen un gran problema. La siguiente image muestra una image simple con un marco de recorte amarillo:

introduzca la descripción de la imagen aquí

El resultado de recorte es:

introduzca la descripción de la imagen aquí

Ahora, cuando se trata de imágenes de retrato, tenemos esta situación:

introduzca la descripción de la imagen aquí

con el siguiente resultado:

introduzca la descripción de la imagen aquí

Entonces, ¿qué pasó aquí? La image original se giró automáticamente hacia la izquierda.

Investigué mucho y básicamente encontré dos sugerencias:

Sugerencia 1

Guarde la orientación de la image antes de recortarla y restaurarla.

 func didTapCropButton(sender: AnyObject) { let originalOrientation = self.imageView.image?.imageOrientation; // raw value of originalOrientation is `3` so its rotated to the right let croppedCGImage = self.imageView.image?.cgImage?.cropping(to: self.cropArea); // create a cropped cgImage let croppedImage = UIImage(cgImage: croppedCGImage!, scale: (self.imageView.image?.scale)!, orientation: (originalOrientation)!); // create the UIImage with the result from cgImage cropping and original orientation if (self.callback != nil) { self.callback.croppingDone(image: croppedImage); } self.dismiss(animated: true, completion: nil); } 

Pero el resultado ahora es:

introduzca la descripción de la imagen aquí

Así que obviamente esta sugerencia no funciona porque simplemente está girando hacia atrás la image ya recortada.

Sugerencia 2

Fijación de orientación. Encontré la siguiente pieza de código con la promise de que arreglaría el error:

 func didTapCropButton(sender: AnyObject) { let image = self.imageView.image?.fixOrientation(); let croppedCGImage = image?.cgImage?.cropping(to: self.cropArea); let croppedImage = UIImage(cgImage: croppedCGImage!); if (self.callback != nil) { self.callback.croppingDone(image: croppedImage); } self.dismiss(animated: true, completion: nil); } extension UIImage { /// Extension to fix orientation of an UIImage without EXIF func fixOrientation() -> UIImage { guard let cgImage = cgImage else { return self } if imageOrientation == .up { return self } var transform = CGAffineTransform.identity switch imageOrientation { case .down, .downMirronetworking: transform = transform.translatedBy(x: size.width, y: size.height) transform = transform.rotated(by: CGFloat(M_PI)) case .left, .leftMirronetworking: transform = transform.translatedBy(x: size.width, y: 0) transform = transform.rotated(by: CGFloat(M_PI_2)) case .right, .rightMirronetworking: transform = transform.translatedBy(x: 0, y: size.height) transform = transform.rotated(by: CGFloat(-M_PI_2)) case .up, .upMirronetworking: break } switch imageOrientation { case .upMirronetworking, .downMirronetworking: transform.translatedBy(x: size.width, y: 0) transform.scaledBy(x: -1, y: 1) case .leftMirronetworking, .rightMirronetworking: transform.translatedBy(x: size.height, y: 0) transform.scaledBy(x: -1, y: 1) case .up, .down, .left, .right: break } if let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: cgImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) { ctx.concatenate(transform) switch imageOrientation { case .left, .leftMirronetworking, .right, .rightMirronetworking: ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) default: ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) } if let finalImage = ctx.makeImage() { return (UIImage(cgImage: finalImage)) } } // something failed -- return original return self } } 

Pero esto da como resultado un área de cultivo incorrecta. El resultado ahora podría ser algo como:

introduzca la descripción de la imagen aquí

Entonces, ¿qué podría ser una solución real para ese problema? De todos modos, ¿cuál es la sensación de girar la image automáticamente si el usuario no la quiere? ¿Es posible deshabilitar esta rotation automática?

EDITAR

Fuente completa de mi Cropper:

 import Foundation import UIKit protocol CropperCallback { func croppingDone(image: UIImage); func croppingCancelled(); } class CropperViewController : UIViewController { @IBOutlet var imageView: UIImageView!; var imageViewScaleCurrent: CGFloat! = 1.0; var imageViewScaleMin: CGFloat! = 0.5; var imageViewScaleMax: CGFloat! = 5.0; @IBOutlet var cropAreaView: CropAreaView!; @IBOutlet weak var cropAreaViewConstraintWidth: NSLayoutConstraint! @IBOutlet weak var cropAreaViewConstraintHeight: NSLayoutConstraint! @IBOutlet var btnCrop: UIButton!; @IBOutlet var btnCancel: UIButton!; var callback: CropperCallback! = nil; var image: UIImage! = nil; var imageOriginalWidth: CGFloat!; var imageOriginalHeight: CGFloat!; var cropWidth: CGFloat! = 287;/ var cropHeight: CGFloat! = 292; var cropHeightFix: CGFloat! = 1.0; var cropArea: CGRect { get { let factor = self.imageView.image!.size.width / self.view.frame.width; let scale = 1 / self.imageViewScaleCurrent; let x = (self.cropAreaView.frame.origin.x - self.imageView.frame.origin.x) * scale * factor; let y = (self.cropAreaView.frame.origin.y - self.imageView.frame.origin.y) * scale * factor; let width = self.cropAreaView.frame.size.width * scale * factor; let height = self.cropAreaView.frame.size.height * scale * factor; return CGRect(x: x, y: y, width: width, height: height); } } static func storyboardInstance() -> CropperViewController? { let storyboard = UIStoryboard(name: String(describing: NSStringFromClass(CropperViewController.classForCoder()).components(separatedBy: ".").last!), bundle: nil); return storyboard.instantiateInitialViewController() as? CropperViewController; } override func viewDidLoad() { super.viewDidLoad(); /* if (self.image.imageOrientation != .up) { self.image = UIImage(cgImage: self.image.cgImage!, scale: self.image.scale, orientation: UIImageOrientation(rawValue: 0)!); } */ self.imageView.image = self.image; self.imageView.isUserInteractionEnabled = true; self.imageView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))); self.imageView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))); self.cropAreaViewConstraintWidth.constant = self.cropWidth; self.cropAreaViewConstraintHeight.constant = self.cropHeight; self.btnCrop.addTarget(self, action: #selector(self.didTapCropButton), for: UIControlEvents.touchUpInside); self.btnCancel.addTarget(self, action: #selector(self.didTapCancelButton), for: UIControlEvents.touchUpInside); } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews(); let imageOriginalRect = self.getRectOfImageInImageView(imageView: self.imageView); self.imageOriginalWidth = imageOriginalRect.size.width; self.imageOriginalHeight = imageOriginalRect.size.height; self.createOverlay(); } func createOverlay() { let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)); let pathRect = UIBezierPath(rect: CGRect(x: self.cropAreaView.frame.origin.x, y: self.cropAreaView.frame.origin.y, width: self.cropWidth, height: self.cropHeight)); path.append(pathRect); path.usesEvenOddFillRule = true; let fillLayer = CAShapeLayer(); fillLayer.path = path.cgPath; fillLayer.fillRule = kCAFillRuleEvenOdd; fillLayer.fillColor = UIColor.white.cgColor; fillLayer.opacity = 0.1; self.view.layer.addSublayer(fillLayer); } func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { let rect = self.getRectOfImageInImageView(imageView: self.imageView); let xImage = rect.origin.x; let yImage = rect.origin.y; let widthImage = rect.size.width; let heightImage = rect.size.height; let xCropView = self.cropAreaView.frame.origin.x; let yCropView = self.cropAreaView.frame.origin.y; let widthCropView = self.cropAreaView.frame.size.width; let heightCropView = self.cropAreaView.frame.size.height; let translation = gestureRecognizer.translation(in: self.view); var x: CGFloat; var y: CGFloat; if (translation.x > 0) { if (!(xImage >= xCropView)) { x = gestureRecognizer.view!.center.x + translation.x; } else { x = gestureRecognizer.view!.center.x; } } else if (translation.x < 0) { if (!((xImage + widthImage) <= (xCropView + widthCropView))) { x = gestureRecognizer.view!.center.x + translation.x; } else { x = gestureRecognizer.view!.center.x; } } else { x = gestureRecognizer.view!.center.x; } if (translation.y > 0) { if (!(yImage >= (yCropView - self.cropHeightFix))) { y = gestureRecognizer.view!.center.y + translation.y; } else { y = gestureRecognizer.view!.center.y; } } else if (translation.y < 0) { if (!((yImage + heightImage) <= (yCropView + heightCropView + self.cropHeightFix))) { y = gestureRecognizer.view!.center.y + translation.y; } else { y = gestureRecognizer.view!.center.y; } } else { y = gestureRecognizer.view!.center.y; } gestureRecognizer.view!.center = CGPoint(x: x, y: y); gestureRecognizer.setTranslation(CGPoint.zero, in: self.view); self.fixImageViewPosition(); } } func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { if let view = gestureRecognizer.view { let widthCropView = self.cropAreaView.frame.size.width; let heightCropView = self.cropAreaView.frame.size.height; if (((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalWidth) > widthCropView) && ((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalHeight) > (heightCropView + (2 * self.cropHeightFix))) && ((self.imageViewScaleCurrent * gestureRecognizer.scale) < self.imageViewScaleMax)) { self.imageViewScaleCurrent = self.imageViewScaleCurrent * gestureRecognizer.scale; view.transform = CGAffineTransform(scaleX: self.imageViewScaleCurrent, y: self.imageViewScaleCurrent); } gestureRecognizer.scale = 1.0; self.fixImageViewPosition(); } } func fixImageViewPosition() { let rect = self.getRectOfImageInImageView(imageView: self.imageView); let xImage = rect.origin.x; let yImage = rect.origin.y; let widthImage = rect.size.width; let heightImage = rect.size.height; let xCropView = self.cropAreaView.frame.origin.x; let yCropView = self.cropAreaView.frame.origin.y; let widthCropView = self.cropAreaView.frame.size.width; let heightCropView = self.cropAreaView.frame.size.height; if (xImage > xCropView) { self.imageView.frame = CGRect(x: xCropView, y: self.imageView.frame.origin.y, width: widthImage, height: heightImage); } if ((xImage + widthImage) < (xCropView + widthCropView)) { self.imageView.frame = CGRect(x: ((xCropView + widthCropView) - widthImage), y: self.imageView.frame.origin.y, width: widthImage, height: heightImage); } if (yImage > yCropView) { self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: (yCropView - self.cropHeightFix), width: widthImage, height: heightImage); } if ((yImage + heightImage) < (yCropView + heightCropView + self.cropHeightFix)) { self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: ((yCropView + heightCropView + self.cropHeightFix) - heightImage), width: widthImage, height: heightImage); } } func getRectOfImageInImageView(imageView: UIImageView) -> CGRect { let imageViewSize = imageView.frame.size; let imageSize = imageView.image!.size; let scaleW = imageViewSize.width / imageSize.width; let scaleH = imageViewSize.height / imageSize.height; let aspect = min(scaleW, scaleH); var imageRect = CGRect(x: 0, y: 0, width: (imageSize.width * aspect), height: (imageSize.height * aspect)); imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2; imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2; imageRect.origin.x += imageView.frame.origin.x; imageRect.origin.y += imageView.frame.origin.y; return imageRect; } func getCGImageWithCorrectOrientation(_ image : UIImage) -> CGImage { if (image.imageOrientation == UIImageOrientation.up) { return image.cgImage!; } var transform : CGAffineTransform = CGAffineTransform.identity; switch (image.imageOrientation) { case UIImageOrientation.right, UIImageOrientation.rightMirronetworking: transform = transform.translatedBy(x: 0, y: image.size.height); transform = transform.rotated(by: CGFloat(-1.0 * M_PI_2)); break; case UIImageOrientation.left, UIImageOrientation.leftMirronetworking: transform = transform.translatedBy(x: image.size.width, y: 0); transform = transform.rotated(by: CGFloat(M_PI_2)); break; case UIImageOrientation.down, UIImageOrientation.downMirronetworking: transform = transform.translatedBy(x: image.size.width, y: image.size.height); transform = transform.rotated(by: CGFloat(M_PI)); break; default: break; } switch (image.imageOrientation) { case UIImageOrientation.rightMirronetworking, UIImageOrientation.leftMirronetworking: transform = transform.translatedBy(x: image.size.height, y: 0); transform = transform.scaledBy(x: -1, y: 1); break; case UIImageOrientation.downMirronetworking, UIImageOrientation.upMirronetworking: transform = transform.translatedBy(x: image.size.width, y: 0); transform = transform.scaledBy(x: -1, y: 1); break; default: break; } let contextWidth : Int; let contextHeight : Int; switch (image.imageOrientation) { case UIImageOrientation.left, UIImageOrientation.leftMirronetworking, UIImageOrientation.right, UIImageOrientation.rightMirronetworking: contextWidth = (image.cgImage?.height)!; contextHeight = (image.cgImage?.width)!; break; default: contextWidth = (image.cgImage?.width)!; contextHeight = (image.cgImage?.height)!; break; } let context : CGContext = CGContext(data: nil, width: contextWidth, height: contextHeight, bitsPerComponent: image.cgImage!.bitsPerComponent, bytesPerRow: image.cgImage!.bytesPerRow, space: image.cgImage!.colorSpace!, bitmapInfo: image.cgImage!.bitmapInfo.rawValue)!; context.concatenate(transform); context.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: CGFloat(contextWidth), height: CGFloat(contextHeight))); let cgImage = context.makeImage(); return cgImage!; } func didTapCropButton(sender: AnyObject) { let fixedImage = self.getCGImageWithCorrectOrientation(self.imageView.image!); // let image = self.imageView.image?.fixOrientation(); let croppedCGImage = fixedImage.cropping(to: self.cropArea); let croppedImage = UIImage(cgImage: croppedCGImage!); if (self.callback != nil) { self.callback.croppingDone(image: croppedImage); } self.dismiss(animated: true, completion: nil); } func didTapCancelButton(sender: AnyObject) { if (self.callback != nil) { self.callback.croppingCancelled(); } self.dismiss(animated: true, completion: nil); } } extension UIImageView { func imageFrame() -> CGRect { let imageViewSize = self.frame.size; guard let imageSize = self.image?.size else { return CGRect.zero; } let imageRatio = imageSize.width / imageSize.height; let imageViewRatio = imageViewSize.width / imageViewSize.height; if (imageRatio < imageViewRatio) { let scaleFactor = imageViewSize.height / imageSize.height; let width = imageSize.width * scaleFactor; let topLeftX = (imageViewSize.width - width) * 0.5; return CGRect(x: topLeftX, y: 0, width: width, height: imageViewSize.height); } else { let scaleFactor = imageViewSize.width / imageSize.width; let height = imageSize.height * scaleFactor; let topLeftY = (imageViewSize.height - height) * 0.5; return CGRect(x: 0, y: topLeftY, width: imageViewSize.width, height: height); } } } extension UIImage { // Extension to fix orientation of an UIImage without EXIF func fixOrientation() -> UIImage { guard let cgImage = self.cgImage else { return self; } if self.imageOrientation == .up { return self; } var transform = CGAffineTransform.identity; switch self.imageOrientation { case .down, .downMirronetworking: transform = transform.translatedBy(x: self.size.width, y: self.size.height); transform = transform.rotated(by: CGFloat(M_PI)); case .left, .leftMirronetworking: transform = transform.translatedBy(x: self.size.width, y: 0); transform = transform.rotated(by: CGFloat(M_PI_2)); case .right, .rightMirronetworking: transform = transform.translatedBy(x: 0, y: self.size.height); transform = transform.rotated(by: CGFloat(-M_PI_2)); case .up, .upMirronetworking: break; } switch self.imageOrientation { case .upMirronetworking, .downMirronetworking: transform.translatedBy(x: self.size.width, y: 0); transform.scaledBy(x: -1, y: 1); case .leftMirronetworking, .rightMirronetworking: transform.translatedBy(x: self.size.height, y: 0); transform.scaledBy(x: -1, y: 1); case .up, .down, .left, .right: break; } if let ctx = CGContext(data: nil, width: Int(self.size.width), height: Int(self.size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: cgImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) { ctx.concatenate(transform); switch self.imageOrientation { case .left, .leftMirronetworking, .right, .rightMirronetworking: ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.height, height: self.size.width)); default: ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)); } if let finalImage = ctx.makeImage() { return (UIImage(cgImage: finalImage)); } } // something failed -- return original return self; } } 

Debes comprender las properties de scale y orientation .

Su sugerencia 1 (use la orientación de la image original) es obviamente una sugerencia correcta y funcionaría, si pudiera rotar y escalar su cropArea .

Su sugerencia 2 es buena para manejar la rotation, pero aún tiene que escalar la área cropArea . Actualmente no está manejando la báscula en absoluto.

(Nota menor, rotating cropArea probablemente tenga un mejor performance que rotar toda la image, consulte https://stackoverflow.com/a/14712184/669586 ).

Tienes que:

  1. Escala (multiplica) cropArea por la escala de la image.
  2. Use la escala de image original al crear el resultado

Por ejemplo, si su UIImage tiene un tamaño de 200x100 y tiene una escala de 2x (es una image de retina), su image de cgImage tendrá un tamaño de 400x200 pero aún está trabajando con un área de recorte de 200x100 !

Algo a lo largo de las líneas de:

 func didTapCropButton(sender: AnyObject) { guard let image = self.imageView.image else { return } let cgImage = self.getCGImageWithCorrectOrientation(image); let scaledCropArea = CGRect( x: self.cropArea.x * image.scale, y: self.cropArea.y * image.scale, width: self.cropArea.width * image.scale, height: self.cropArea.height * image.scale ) let croppedCGImage = cgImage.cropping(to: scaledCropArea) let croppedImage = UIImage(cgImage: croppedCGImage!, scale: image.scale, orientation: .up) if (self.callback != nil) { self.callback.croppingDone(image: croppedImage) } self.dismiss(animated: true, completion: nil) } 

La rotation automática y las transformaciones en UIImage son simplemente una optimization. Gracias a esta optimization, varias imágenes pueden compartir el mismo almacenamiento (los mismos datos de memory). La optimization ya está realizada en su cargador de resources y no puede desactivarla.

Además, consulte https://stackoverflow.com/a/18602671/669586 para una implementación más simple y segura.