Squeezing/Squashing of Images in iOS Swift

350 views Asked by At

I am working on a photo editing application where I need to squeeze/squash car images to create cute caricatures just like the image attached. This is my very first photo editing application so I don't exactly know what term to put on Google search and even how I can achieve this thing. Any help, guidance, or direction would be a great favor.

Please note this is not related to image resizing.

enter image description here

Following are two more examples.

enter image description here enter image description here

enter image description here

1

There are 1 answers

2
DonMag On

There are a couple ways to do this...

If we use this as the original image:

enter image description here

it has a transparent bounding box... so it looks like this in an image editing program:

enter image description here

If you are only dealing with how it looks at run-time, you can use a UIImageView with Content Mode: Scale to Fill and then adjust the width of the image view.

If you want to actually scale the UIImage, this simple extension will do the job:

extension UIImage {
    func squeeze(w: CGFloat, h: CGFloat) -> UIImage {
        let newWidth: CGFloat = self.width * w
        let newHeight: CGFloat = self.height * h
        let sz: CGSize = CGSize(width: newWidth, height: newHeight)
        let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(size: sz)
        let img = renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: sz))
        }
        return img
    }
}

enter image description here

(the image views have a green border, so we can see the frames).

Here is some example code you can play with:

class ViewController: UIViewController, UITextFieldDelegate {
    
    var origIMG: UIImage!
    
    let imgViewA = UIImageView()
    let imgViewB = UIImageView()
    let imgViewC = UIImageView()
    
    let st = UIStackView()
    
    let statusLabel = UILabel()
    let slider = UISlider()
    
    var bWidth: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let img = UIImage(named: "car1"),
              let bkgImg = UIImage(named: "carsBKG")
        else { return }
        print(img.size)
        origIMG = img
        
        // add a background image view
        let vBKG = UIImageView(image: bkgImg)
        
        st.axis = .vertical
        st.alignment = .center
        st.spacing = 2
        
        [imgViewA, imgViewB, imgViewC].forEach { v in
            v.layer.borderWidth = 2
            v.layer.borderColor = UIColor.green.cgColor
            v.backgroundColor = .clear
            v.image = origIMG
        }
        
        // for A and B, we let the image view frame scale the image
        imgViewA.contentMode = .scaleToFill
        imgViewB.contentMode = .scaleToFill
        // for C we keep the image aspect ratio, because
        //  we're going to modify the UIImage size
        imgViewC.contentMode = .scaleAspectFit
        
        var label: UILabel!
        
        label = UILabel()
        label.text = "Original"
        st.addArrangedSubview(label)
        st.addArrangedSubview(imgViewA)
        st.setCustomSpacing(20.0, after: imgViewA)
        
        label = UILabel()
        label.text = "Modify Image View Frame"
        st.addArrangedSubview(label)
        st.addArrangedSubview(imgViewB)
        st.setCustomSpacing(20.0, after: imgViewB)
        
        label = UILabel()
        label.text = "Modify UIImage"
        st.addArrangedSubview(label)
        st.addArrangedSubview(imgViewC)
        st.setCustomSpacing(20.0, after: imgViewC)
        
        statusLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        statusLabel.textAlignment = .center
        st.addArrangedSubview(statusLabel)
        
        slider.backgroundColor = .white.withAlphaComponent(0.6)
        slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
        st.addArrangedSubview(slider)
        st.setCustomSpacing(20.0, after: slider)
        
        vBKG.translatesAutoresizingMaskIntoConstraints = false
        st.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(vBKG)
        view.addSubview(st)
        
        // we're going to modify the 2nd image view width
        bWidth = imgViewB.widthAnchor.constraint(equalTo: st.widthAnchor)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            st.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            st.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            st.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            
            // background image view same size/position as stack view
            //  plus 20-points on each side
            vBKG.topAnchor.constraint(equalTo: st.topAnchor, constant: -20.0),
            vBKG.leadingAnchor.constraint(equalTo: st.leadingAnchor, constant: -20.0),
            vBKG.trailingAnchor.constraint(equalTo: st.trailingAnchor, constant: 20.0),
            vBKG.bottomAnchor.constraint(equalTo: st.bottomAnchor, constant: 20.0),
            
            // top image (original) full width, proportional height
            imgViewA.widthAnchor.constraint(equalTo: st.widthAnchor),
            imgViewA.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
            
            // 2nd image starts at full width, proportional height
            //  we'll change the image VIEW width
            bWidth,
            imgViewB.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
            
            // 3rd image full width, proportional height
            //  we'll change the actual IMAGE
            imgViewC.widthAnchor.constraint(equalTo: st.widthAnchor),
            imgViewC.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
            
            slider.widthAnchor.constraint(equalTo: st.widthAnchor),
        ])
        
        // start slider at 100%
        slider.value = 1.0
        
        statusLabel.text = "100%"
        
        // let's make the labels readable
        st.arrangedSubviews.forEach { vSub in
            if let v = vSub as? UILabel {
                v.textColor = .white
                v.backgroundColor = .black.withAlphaComponent(0.4)
                v.textAlignment = .center
                v.widthAnchor.constraint(equalTo: st.widthAnchor).isActive = true
            }
        }

    }
    
    @objc func sliderChanged(_ sender: UISlider) {
        let pct = CGFloat(sender.value)
        // let's use a minimum scale of 0.5%
        if pct < 0.05 { return }
        
        // update imgViewB's width anchor as a percentage
        bWidth.isActive = false
        bWidth = imgViewB.widthAnchor.constraint(equalTo: st.widthAnchor, multiplier: pct)
        bWidth.isActive = true
        
        // set imgViewC's image to a NEW rendered UIImage
        //  we're scaling the width, keeping the original height
        imgViewC.image = origIMG.squeeze(w: pct, h: 1.0)
        
        statusLabel.text = "\(Int(pct * 100))%"
    }
    
}


extension UIImage {
    func squeeze(w: CGFloat, h: CGFloat) -> UIImage {
        let newWidth: CGFloat = self.width * w
        let newHeight: CGFloat = self.height * h
        let sz: CGSize = CGSize(width: newWidth, height: newHeight)
        let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(size: sz)
        let img = renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: sz))
        }
        return img
    }
}

When running, it looks like this - dragging the slider sets the Widths to a percentage of the original:

enter image description here

enter image description here

enter image description here