Crop screenshot image with drawn rectangle

71 views Asked by At

I am first drawing a rectangle on the screen with "UITouch". Afterwards i am taking a screenshot of the whole screen and attempting to crop the image with the positioning of the drawn rectangle.

All my attempts crop the image in an incorrect position. What am i missing? How can this be achieved?

1: Drawing a rectangle and storing the drawn position as global variables.

func drawSelectionArea(fromPoint: CGPoint, toPoint: CGPoint) {
        let rect = CGRect(x: min(fromPoint.x, toPoint.x),
                          y: min(fromPoint.y, toPoint.y),
                          width: abs(fromPoint.x - toPoint.x),
                          height: abs(fromPoint.y - toPoint.y));
        
        overlay.frame = rect
        
        xCurrent = min(fromPoint.x, toPoint.x)
        yCurrent = min(fromPoint.y, toPoint.y)
        widthCurrent = abs(fromPoint.x - toPoint.x)
        heightCurrent = abs(fromPoint.y - toPoint.y)    
    }

2: Taking a screenshot of the whole screen.

    @IBAction func createScreenshot(_ sender: Any) {
       let imageSize = UIScreen.main.bounds.size as CGSize;
       UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
       let context = UIGraphicsGetCurrentContext()
       for obj : AnyObject in UIApplication.shared.windows {
           if let window = obj as? UIWindow {
               if window.responds(to: #selector(getter: UIWindow.screen)) || window.screen == UIScreen.main {

                   context!.saveGState();
                   context!.translateBy(x: window.center.x, y: window.center.y);
                   context!.concatenate(window.transform);
                   context!.translateBy(x: -window.bounds.size.width * window.layer.anchorPoint.x,
                                        y: -window.bounds.size.height * window.layer.anchorPoint.y);

                   window.layer.render(in: context!)
                   context!.restoreGState();
               }
           }
       }
       let imageContext = UIGraphicsGetImageFromCurrentImageContext();
       let image = self.cropImage(screenshot: imageContext!)
   }

3: Attempting to crop the screenshot with the same positioning as the drawn rectangle.

  func cropImage(screenshot: UIImage) -> UIImage {
        let crop = CGRectMake(self.xCurrent, self.yCurrent,
                              self.widthCurrent,
                              self.heightCurrent)

        let cgImage = screenshot.cgImage!.cropping(to: crop)
        let image: UIImage = UIImage(cgImage: cgImage!)
        return image
    }
1

There are 1 answers

5
DonMag On BEST ANSWER

You should be able to get rid of almost all of that code...

Try replacing your func with this:

@IBAction func createScreenshot(_ sender: Any) {
    
    // hide the overlay while we capture the view
    overlay.isHidden = true
    
    let renderRect: CGRect = overlay.frame
    let rndr = UIGraphicsImageRenderer(bounds: renderRect)
    let croppedImage = rndr.image { ctx in
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
    }
    
    // show the overlay again
    overlay.isHidden = false
    
    // do something with croppedImage
    
}

Edit - based on comments...

OK, as I understand your goal now, you want to present a view controller (over current context) with a clear background. That controller will have the "drawable" overlay view to define the area to capture ... along with, I'm assuming, a "Capture" button.

You can still use the above code - but you'll want to get a reference to the view from the controller that presented the overlay-view controller:

@IBAction func createScreenshot(_ sender: Any) {
    // make sure we have been presented and
    //  get a reference to the *presenting* controller's view
    guard let pc = self.presentingViewController, let v = pc.view else { return }
    
    let renderRect: CGRect = self.overlay.frame
    let rndr = UIGraphicsImageRenderer(bounds: renderRect)
    let croppedImage = rndr.image { ctx in
        // draw the presenting controller's view
        v.drawHierarchy(in: v.bounds, afterScreenUpdates: true)
    }
    
    // do something with the croppedImage
}

Here's a quick example...

We'll start with a view controller containing a button and a "grid" of labels:

class CropShotVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // let's fill the view with a grid of labels
        var colors: [[UIColor]] = [
            [.gray, .white], [.systemGreen, .white], [.systemBlue, .white],
            [.cyan, .black], [.yellow, .black], [.magenta, .black],
            [.green, .black], [.blue, .white], [.systemYellow, .black],
        ]
        
        let stack = UIStackView()
        stack.axis = .vertical
        stack.distribution = .fillEqually
        
        for r in 1...6 {
            let rowStack = UIStackView()
            rowStack.distribution = .fillEqually
            for c in 1...3 {
                let v = UILabel()
                v.numberOfLines = 0
                v.textAlignment = .center
                v.text = "Row: \(r)\nCol: \(c)"
                let colorPair = colors.removeFirst()
                v.backgroundColor = colorPair[0]
                v.textColor = colorPair[1]
                colors.append(colorPair)
                rowStack.addArrangedSubview(v)
            }
            stack.addArrangedSubview(rowStack)
        }
        
        // add a white "panel" view at the top
        //  to hold the "Present Capture VC" button
        let panel = UIView()
        panel.backgroundColor = .white
        
        var cfg = UIButton.Configuration.filled()
        cfg.title = "Present Capture VC"
        let btnA = UIButton(configuration: cfg)
        btnA.addAction (
            UIAction { _ in
                if self.presentedViewController != nil {
                    return
                }
                let vc = RectOverVC()
                vc.modalPresentationStyle = .overCurrentContext
                self.present(vc, animated: true)
            }, for: .touchUpInside
        )
        btnA.setContentHuggingPriority(.required, for: .vertical)
        
        panel.translatesAutoresizingMaskIntoConstraints = false
        btnA.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(panel)
        view.addSubview(btnA)
        
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)
        
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            panel.topAnchor.constraint(equalTo: g.topAnchor),
            panel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            panel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            btnA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            btnA.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            btnA.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -8.0),
            
            stack.topAnchor.constraint(equalTo: panel.bottomAnchor, constant: 0.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
        ])
        
        view.backgroundColor = .systemBackground
        
    }
    
}

This is the controller we will present, where the user can define the capture area (for this example, not "sizable" ... just a draggable view):

class RectOverVC: UIViewController {
    
    let overlay: UIView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .clear
        
        // add a translucent white "panel" view across the top
        let panel = UIView()
        panel.backgroundColor = .white.withAlphaComponent(0.8)
        
        // we'll add "Cancel" and "Capture" buttons
        var cfg = UIButton.Configuration.filled()
        
        cfg.title = "Cancel"
        let btnA = UIButton(configuration: cfg)
        btnA.addAction (
            UIAction { _ in
                self.dismiss(animated: true)
            }, for: .touchUpInside
        )
        
        cfg.title = "Capture"
        let btnB = UIButton(configuration: cfg)
        btnB.addAction (
            UIAction { _ in
                self.createScreenshot(self)
            }, for: .touchUpInside
        )
        
        panel.translatesAutoresizingMaskIntoConstraints = false
        btnA.translatesAutoresizingMaskIntoConstraints = false
        btnB.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(panel)
        panel.addSubview(btnA)
        panel.addSubview(btnB)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            panel.topAnchor.constraint(equalTo: g.topAnchor),
            panel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            panel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            btnA.topAnchor.constraint(equalTo: panel.topAnchor, constant: 8.0),
            btnA.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 8.0),
            
            btnB.topAnchor.constraint(equalTo: panel.topAnchor, constant: 8.0),
            btnB.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -8.0),
            
            btnB.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -8.0),
            
        ])
        
        // add a red-bordered draggable view
        //  to select the capture area
        // we'll give it a translucent-white background to make it easy to see
        overlay.backgroundColor = .white.withAlphaComponent(0.60)
        overlay.layer.borderColor = UIColor.red.cgColor
        overlay.layer.borderWidth = 2
        view.addSubview(overlay)
        
        // pan gesture to make the overlay view draggable
        let pg = UIPanGestureRecognizer(target: self, action: #selector(panView(_:)))
        overlay.addGestureRecognizer(pg)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if overlay.frame == .zero {
            // start the overlayView centered
            overlay.frame = .init(origin: .zero, size: .init(width: 160.0, height: 240.0))
            overlay.center = view.center
        }
    }
    
    @objc func panView(_ sender: UIPanGestureRecognizer) {
        guard let v = sender.view else { return }
        let translation = sender.translation(in: self.view)
        v.frame = v.frame.offsetBy(dx: translation.x, dy: translation.y)
        sender.setTranslation(CGPoint(x: 0, y: 0), in: v)
    }
    
    @IBAction func createScreenshot(_ sender: Any) {
        // make sure we have been presented and
        //  get a reference to the *presenting* controller's view
        guard let pc = self.presentingViewController, let v = pc.view else { return }
        
        let renderRect: CGRect = self.overlay.frame
        let rndr = UIGraphicsImageRenderer(bounds: renderRect)
        let croppedImage = rndr.image { ctx in
            // draw the presenting controller's view
            v.drawHierarchy(in: v.bounds, afterScreenUpdates: true)
        }
        
        // do something with the croppedImage
        //  for this example, we'll show it in a presented controller
        
        // present another controller to show the captured UIImage
        let vc = ShowCroppedImageVC()
        vc.theImage = croppedImage
        self.present(vc, animated: true)
    }
}

and a view controller to display the resulting UIImage:

class ShowCroppedImageVC: UIViewController {
    
    var theImage: UIImage?
    let imgView = UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black
        
        guard let img = theImage else { return }

        // dark-gray view to use as a "frame" for the image view
        let frameView = UIView()
        frameView.backgroundColor = .darkGray

        let imgView = UIImageView(image: img)
        
        [frameView, imgView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        frameView.addSubview(imgView)
        view.addSubview(frameView)

        NSLayoutConstraint.activate([
            imgView.widthAnchor.constraint(equalToConstant: 160.0),
            imgView.heightAnchor.constraint(equalToConstant: 240.0),
            frameView.widthAnchor.constraint(equalTo: imgView.widthAnchor, constant: 40.0),
            frameView.heightAnchor.constraint(equalTo: imgView.heightAnchor, constant: 40.0),
            imgView.centerXAnchor.constraint(equalTo: frameView.centerXAnchor),
            imgView.centerYAnchor.constraint(equalTo: frameView.centerYAnchor),
            frameView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            frameView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
    
}

It will look like this on launch:

enter image description here

Tapping "Present Capture VC" will look like this (capture area starts centered):

enter image description here

Here I've dragged the rect to the lower-right:

enter image description here

and tapping "Capture" will display the captured UIImage:

enter image description here