Search code examples
iosswiftscreenshotcgimageuigraphicscontext

Crop screenshot image with drawn rectangle


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
    }

Solution

  • 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