web-dev-qa-db-ja.com

cgImage?.croppingが画像を回転させるのはなぜですか?

UICropperViewControllerを作成しましたが、横向きモードの画像で完全に機能しています。ポートレートモードの画像には大きな問題があります。次の図は、黄色のクロップフレームを使用した単純な画像を示しています。

enter image description here

トリミング結果は次のとおりです。

enter image description here

ポートレート画像に関しては、次のような状況になります。

enter image description here

次の結果が得られます。

enter image description here

では、ここで何が起こったのでしょうか。元の画像は自動的に左に回転しました。

私はたくさん調べて、基本的に2つの提案を見つけました:

提案1

トリミングする前に画像の向きを保存して復元します。

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

しかし、結果は次のようになります。

enter image description here

したがって、この提案は、すでにトリミングされた画像を単に回転して戻すだけなので、明らかに機能しません。

提案2

向きの修正。エラーが修正されることを約束して、次のコードを見つけました。

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, .downMirrored:
            transform = transform.translatedBy(x: size.width, y: size.height)
            transform = transform.rotated(by: CGFloat(M_PI))

        case .left, .leftMirrored:
            transform = transform.translatedBy(x: size.width, y: 0)
            transform = transform.rotated(by: CGFloat(M_PI_2))

        case .right, .rightMirrored:
            transform = transform.translatedBy(x: 0, y: size.height)
            transform = transform.rotated(by: CGFloat(-M_PI_2))

        case .up, .upMirrored:
            break
        }

        switch imageOrientation {

        case .upMirrored, .downMirrored:
            transform.translatedBy(x: size.width, y: 0)
            transform.scaledBy(x: -1, y: 1)

        case .leftMirrored, .rightMirrored:
            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, .leftMirrored, .right, .rightMirrored:
                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
    }
}

しかし、これは間違った作付面積になります。結果は次のようになります。

enter image description here

では、その問題の本当の解決策は何でしょうか?とにかく、ユーザーがそれを望まない場合、画像を自動的に回転させる意味は何ですか?この自動回転を無効にすることは可能ですか?

[〜#〜]編集[〜#〜]

私の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.rightMirrored:
            transform = transform.translatedBy(x: 0, y: image.size.height);
            transform = transform.rotated(by: CGFloat(-1.0 * M_PI_2));
            break;
        case UIImageOrientation.left, UIImageOrientation.leftMirrored:
            transform = transform.translatedBy(x: image.size.width, y: 0);
            transform = transform.rotated(by: CGFloat(M_PI_2));
            break;
        case UIImageOrientation.down, UIImageOrientation.downMirrored:
            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.rightMirrored, UIImageOrientation.leftMirrored:
            transform = transform.translatedBy(x: image.size.height, y: 0);
            transform = transform.scaledBy(x: -1, y: 1);
            break;
        case UIImageOrientation.downMirrored, UIImageOrientation.upMirrored:
            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.leftMirrored,
             UIImageOrientation.right, UIImageOrientation.rightMirrored:
            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, .downMirrored:
            transform = transform.translatedBy(x: self.size.width, y: self.size.height);
            transform = transform.rotated(by: CGFloat(M_PI));
        case .left, .leftMirrored:
            transform = transform.translatedBy(x: self.size.width, y: 0);
            transform = transform.rotated(by: CGFloat(M_PI_2));
        case .right, .rightMirrored:
            transform = transform.translatedBy(x: 0, y: self.size.height);
            transform = transform.rotated(by: CGFloat(-M_PI_2));
        case .up, .upMirrored:
            break;
        }

        switch self.imageOrientation {
        case .upMirrored, .downMirrored:
            transform.translatedBy(x: self.size.width, y: 0);
            transform.scaledBy(x: -1, y: 1);
        case .leftMirrored, .rightMirrored:
            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, .leftMirrored, .right, .rightMirrored:
                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;
    }
}
13
Mulgard

scaleおよびorientationプロパティを理解する必要があります。

提案1(元の画像の向きを使用)は明らかに正しい提案であり、cropAreaを回転および拡大縮小することもできれば機能します。

あなたの提案2はローテーションを処理するのに適していますが、それでもcropAreascaleする必要があります。現在、あなたはスケールをまったく扱っていません。

(マイナーな注意、cropAreaを回転させると、画像全体を回転させるよりもパフォーマンスが向上する可能性があります。 https://stackoverflow.com/a/14712184/669586 を参照してください)。

必ず:

  1. スケール(乗算)cropAreaに画像のスケールを掛けます。
  2. 結果を作成するときは、元の画像スケールを使用してください

たとえば、UIImageのサイズが200x100で、スケールが2x(網膜画像)の場合、cgImageのサイズは400x200になりますが、あなたはまだ200x100内の作物エリアで作業しています!

次のようなもの:

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

UIImageの自動回転と変換は、単なる最適化です。この最適化のおかげで、複数のイメージが同じストレージ(同じメモリデータ)を共有できます。最適化はアセットローダーですでに行われており、無効にすることはできません。

また、より簡単で安全な実装については、 https://stackoverflow.com/a/18602671/669586 を参照してください。

8
Sulthan