iosswiftuitextviewcgcontextdrawimage

iOS TextView is saved blurry when scaled


I tried save textview as image with not device scale. I implemented a method to save an image by adding an arbitrary textview according to the UI value. Because when I tried save image using drawHierarchy method in up scale, image was blurry.

Condition when textview is saved blurry

here is my code

func drawQuoteImage() {
    var campusSize = view.frame.size 
    var scale = UIScreen.main.scale + 2 

    // 1. Create View 
    let quoteView = UIView(frame: CGRect(x: 0, y: 0, width: campusSize.width, height: campusSize.height))
    let textview = UITextView()
    textview.attributedText = NSAttributedString(string: quoteLabel.text, attributes: textAttributes as [NSAttributedString.Key : Any])
    textview.frame = transfromFrame(originalFrame: quoteLabel.frame, campusSize: campusSize)
    quoteView.addSubview(textview)

    // 2. Render image
    UIGraphicsBeginImageContextWithOptions(quoteView.frame.size, false, scale)
    let context = UIGraphicsGetCurrentContext()!
    context.setRenderingIntent(.relativeColorimetric)
    context.interpolationQuality = .high
    quoteView.drawHierarchy(in: quoteView.frame, afterScreenUpdates: true)
    quoteView.layer.render(in: context)
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    quoteImage = image 
}

private func transfromFrame(originalFrame: CGRect, campusSize: CGSize) -> CGRect
{
    if UIDevice.current.screenType == .iPhones_X_XS {
        return CGRect(x: round(originalFrame.origin.x), y: round(originalFrame.origin.y), width: round(originalFrame.width), height: round(originalFrame.height))
    }
    else {
        var frame = CGRect()
        let ratioBasedOnWidth = campusSize.width / editView.frame.width
        let ratioBasedOnHeight = campusSize.height / editView.frame.height
        frame.size.width = round(originalFrame.width * ratioBasedOnWidth)
        frame.size.height = round(originalFrame.height * ratioBasedOnHeight)
        frame.origin.x = round(originalFrame.origin.x * ratioBasedOnWidth)
        frame.origin.y = round(originalFrame.origin.y * ratioBasedOnHeight)
        return frame
    }
}

Wired Point

when height of textview is more than 128, textview is save blurry. I found related value when I put textview default height is 128.

enter image description here

Height is 128 or less (when isScrollEnabled is false), textview is saved always clear. But when height is more than 128, it looks blurry.

Height 128

enter image description here

Height 129

enter image description here

I'd like to know how to clearly draw image with textview at @5x scale. (textview height is bigger than 128)


Solution

  • Here's a quick example using a UIView extension from this accepted answer: https://stackoverflow.com/a/51944513/6257435

    We'll create a UITextView with a size of 240 x 129. Then add 4 buttons to capture the text view at 1x, 2x, 5x and 10x scale.

    It looks like this when running:

    enter image description here

    and the result...

    At 1x scale - 240 x 129 pixels:

    enter image description here

    At 2x scale - 480 x 258 pixels:

    enter image description here

    At 5x scale - 1200 x 645 pixels (just showing a portion):

    enter image description here

    At 10x scale - 2400 x 1290 pixels (just showing a portion):

    enter image description here

    The extension:

    extension UIView {
        func scale(by scale: CGFloat) {
            self.contentScaleFactor = scale
            for subview in self.subviews {
                subview.scale(by: scale)
            }
        }
        
        func getImage(scale: CGFloat? = nil) -> UIImage {
            let newScale = scale ?? UIScreen.main.scale
            self.scale(by: newScale)
            
            let format = UIGraphicsImageRendererFormat()
            format.scale = newScale
            
            let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
            
            let image = renderer.image { rendererContext in
                self.layer.render(in: rendererContext.cgContext)
            }
            
            return image
        }
    }
    

    Sample controller code:

    class TextViewCapVC: UIViewController {
        let textView = UITextView()
        let resultLabel = UILabel()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // add a stack view with buttons
            let stack = UIStackView()
            stack.axis = .vertical
            stack.spacing = 12
            
            [1, 2, 5, 10].forEach { i in
                let btn = UIButton()
                btn.setTitle("Create Image at \(i)x scale", for: [])
                btn.setTitleColor(.white, for: .normal)
                btn.setTitleColor(.lightGray, for: .highlighted)
                btn.backgroundColor = .systemBlue
                btn.tag = i
                btn.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
                stack.addArrangedSubview(btn)
            }
            
            [textView, stack, resultLabel].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                // text view 280x240, 20-points from top, centered horizontally
                textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                textView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                textView.widthAnchor.constraint(equalToConstant: 240.0),
                textView.heightAnchor.constraint(equalToConstant: 129.0),
                
                // stack view, 20-points from text view, same width, centered horizontally
                stack.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20.0),
                stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                stack.widthAnchor.constraint(equalTo: textView.widthAnchor),
                
                // result label, 20-points from stack view
                //  20-points from leading/trailing
                resultLabel.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
                resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
            ])
            
            let string = "Test"
            
            let attributes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.blue,
                .font: UIFont.italicSystemFont(ofSize: 104.0),
            ]
            
            let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
            textView.attributedText = attributedString
            
            resultLabel.font = .systemFont(ofSize: 14, weight: .light)
            resultLabel.numberOfLines = 0
            resultLabel.text = "Results:"
            
            // so we can see the view frames
            textView.backgroundColor = .yellow
            resultLabel.backgroundColor = .cyan
            
        }
        
        @objc func gotTap(_ sender: Any?) {
            guard let btn = sender as? UIButton else { return }
            
            let scaleFactor = CGFloat(btn.tag)
            
            let img = textView.getImage(scale: scaleFactor)
            
            var s: String = "Results:\n\n"
            
            let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let fName: String = "\(btn.tag)xScale-\(img.size.width * img.scale)x\(img.size.height * img.scale).png"
            let url = documents.appendingPathComponent(fName)
            if let data = img.pngData() {
                do {
                    try data.write(to: url)
                } catch {
                    s += "Unable to Write Image Data to Disk"
                    resultLabel.text = s
                    return
                }
            } else {
                s += "Could not get png data"
                resultLabel.text = s
                return
            }
            s += "Logical Size: \(img.size)\n\n"
            s += "Scale: \(img.scale)\n\n"
            s += "Pixel Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n\n"
            s += "File \"\(fName)\"\n\nsaved to Documents folder\n"
            resultLabel.text = s
            
            // print the path to documents in debug console
            //  so we can copy/paste into Finder to get to the files
            print(documents.path)
        }
    
    }