skip to Main Content

I am making an application that automatically enters a number when a photo is selected using UIKit and Swift.

enter image description here

The picture above is the most perfect situation and result.

The problem arises in a situation like the picture below.

enter image description here

The problem arises when working with photos of different resolutions. The text is large in some photos and small in others.

enter image description here

The problem I was thinking of is that the font size seems to be different when giving pictures with different resolutions because the font size is given as a fixed value.

// add text in photo
func textToImage(drawText text: String, inImage image: UIImage, fontSize: CGFloat, atPoint point: CGPoint) -> UIImage {
    
    let textStrokeWidth: Double = -4.0
    
    let textStrokeColor: UIColor = UIColor.black
    
    let textFillColor: UIColor = UIColor.white
    
    let textFont = UIFont(name: "Helvetica Bold", size: fontSize)!
    
    UIGraphicsBeginImageContextWithOptions(image.size, false, 1)
    
    let textFontAttributes = [
        NSAttributedString.Key.strokeColor: textStrokeColor,
        NSAttributedString.Key.strokeWidth : textStrokeWidth,
        NSAttributedString.Key.font: textFont,
        NSAttributedString.Key.foregroundColor: textFillColor,
        ] as [NSAttributedString.Key : Any]
    
    
    image.draw(in: CGRect(origin: CGPoint.zero, size: image.size))
    let rect = CGRect(origin: point, size: image.size)
    text.draw(in: rect, withAttributes: textFontAttributes)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage!
}

Source of code used

I modified the code from the above source slightly and tried it. And to solve the scale problem, I am trying ways to calculate the fontsize, but the solution is taking too long than I thought, so I leave a question.

let scaleFactor: Float = Float(imageWidth) / 1500.0
let fontSize = CGFloat(150.0 * scaleFactor)

The calculation method I tried is as follows. When there is a picture with a width of 1500, font size 150 is the best, and the scale factor is calculated based on that.

However, as you can see, it is impossible to cope with all resolutions because only horizontal is used.

enter image description here

How can I find the scale factor that corresponds to all resolutions and calculate the font size of the same ratio even if different photos are input?

You don’t necessarily have to create text this way. I would like to know more about how to solve this problem.

2

Answers


  1. try to get the dynamic font size based on the image width and image height.

    let fontSize = (image.size.width/image.size.height) * 50
    
    Login or Signup to reply.
  2. One approach — with a few drawbacks — but might work for you.

    The general idea…

    • define a "base" font – for example, your "Helvetica Bold" at 150 points
    • get the size of the string using that font
    • calculate an aspect-fit rectangle for that size to fit in the image size
    • use the difference in resulting size to "scale" the base font point size

    Here are the steps:

        // left/right and top/bottom min space
        let padding: CGFloat = 20.0
        
        // maximum rect for the text
        let maxRect: CGRect = CGRect(origin: .zero, size: theImage.size).insetBy(dx: padding, dy: padding)
        
        // get size of theString (single-line) using our baseFont
        let baseSZ: CGSize = baseFont.sizeOfString(string: theString, constrainedToWidth: .greatestFiniteMagnitude)
        
        // aspect-ratio rect that fits inside inset rect
        let fitRect: CGRect = AVMakeRect(aspectRatio: baseSZ, insideRect: maxRect)
        
        // "scaled" font size
        let newPointSize: CGFloat = fitRect.height / baseSZ.height * baseFont.pointSize
        
        // create a new font at calculated point size
        guard let newFont: UIFont = UIFont(name: baseFont.fontName, size: newPointSize) else { return }
        
        let newIMG: UIImage = drawTextOnImage(theString, withFont: newFont, inRect: fitRect, textColor: .yellow, onImage: theImage)
        
    

    So, using this complete code for a sample app:

    import UIKit
    import AVFoundation
    
    class ViewController: UIViewController {
        
        let tfImage = UITextField()
        let tfText = UITextField()
        let tfWidth = UITextField()
        let tfHeight = UITextField()
        let imgView = UIImageView()
        
        var baseFont: UIFont!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // base font - let's use 100-pointSize
            guard let tf = UIFont(name: "Helvetica Bold", size: 100.0) else { return }
            baseFont = tf
            
            [tfImage, tfText, tfWidth, tfHeight].forEach { v in
                v.borderStyle = .roundedRect
                v.textAlignment = .center
                v.autocapitalizationType = .none
                v.autocorrectionType = .no
            }
            
            imgView.contentMode = .scaleAspectFit
            imgView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            imgView.clipsToBounds = true
            
            // horizontal stack view for width/height fields and button
            let hStack = UIStackView()
            hStack.spacing = 8
            hStack.distribution = .fillEqually
            
            var label = UILabel()
            label.textAlignment = .right
            label.text = "width:"
            
            hStack.addArrangedSubview(label)
            hStack.addArrangedSubview(tfWidth)
            
            label = UILabel()
            label.textAlignment = .right
            label.text = "height:"
            
            hStack.addArrangedSubview(label)
            hStack.addArrangedSubview(tfHeight)
            
            let btn = UIButton()
            btn.setTitle("Show", for: [])
            btn.setTitleColor(.white, for: .normal)
            btn.setTitleColor(.lightGray, for: .highlighted)
            btn.backgroundColor = .systemRed
            btn.layer.cornerRadius = 8
            
            hStack.addArrangedSubview(btn)
            
            let stack = UIStackView()
            stack.axis = .vertical
            stack.spacing = 4
    
            label = UILabel()
            label.text = "Image Name:"
            label.font = .systemFont(ofSize: 14.0, weight: .light)
    
            stack.addArrangedSubview(label)
            stack.addArrangedSubview(tfImage)
    
            label = UILabel()
            label.text = "Text to render:"
            label.font = .systemFont(ofSize: 14.0, weight: .light)
            
            stack.addArrangedSubview(label)
            stack.addArrangedSubview(tfText)
            
            stack.addArrangedSubview(hStack)
            stack.addArrangedSubview(imgView)
            
            stack.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(stack)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
                stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 2.0 / 3.0),
            ])
            
            // assign button target/action
            btn.addTarget(self, action: #selector(showImage(_:)), for: .touchUpInside)
            
            // add a tap gesture to the image view so we can
            //  toggle its .contentMode
            let t = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
            imgView.addGestureRecognizer(t)
            imgView.isUserInteractionEnabled = true
            
            // some starting text in the text fields
            tfText.text = "Text : Number 1"
            tfWidth.text = "300"
            tfHeight.text = "300"
        
            //tfImage.text = "bkg640x360"
            
        }
        
        @objc func handleTap(_ sender: UITapGestureRecognizer) {
            // toggle image view .contentMode between
            //  .scaleAspectFit and .center
            imgView.contentMode = imgView.contentMode == .scaleAspectFit ? .center : .scaleAspectFit
        }
        
        @objc func showImage(_ sender: UIButton) {
    
            // make sure we entered a string to render
            guard let theString = tfText.text, !theString.isEmpty else { return }
    
            var theImage: UIImage!
            
            // if we entered an image name, and it can be loaded
            if let imgName = tfImage.text, !imgName.isEmpty,
               let realImage = UIImage(named: imgName) {
                
                // use the loaded image
                
                theImage = realImage
                
            } else {
                // let's generate a solid-color image at the entered size
            
                // make sure we have valid width and height values entered
                guard let sW = tfWidth.text, !sW.isEmpty, let dW = Double(sW),
                      let sH = tfHeight.text, !sH.isEmpty, let dH = Double(sH),
                      dW > 0.0, dH > 0.0
                else { return }
                
                // size of image to generate
                let targetSize: CGSize = CGSize(width: CGFloat(dW), height: CGFloat(dH))
                
                // create a solid-color image of targetSize
                //  for actual use, we'd be using a UIImage from somewhere
                theImage = UIGraphicsImageRenderer(size: targetSize).image { ctx in
                    UIColor.systemBlue.setFill()
                    ctx.fill(CGRect(origin: .zero, size: targetSize))
                }
                
            }
            
            view.endEditing(true)
            
            // left/right and top/bottom min space
            let padding: CGFloat = 20.0
            
            // maximum rect for the text
            let maxRect: CGRect = CGRect(origin: .zero, size: theImage.size).insetBy(dx: padding, dy: padding)
            
            // get size of theString (single-line) using our baseFont
            let baseSZ: CGSize = baseFont.sizeOfString(string: theString, constrainedToWidth: .greatestFiniteMagnitude)
            
            // aspect-ratio rect that fits inside inset rect
            let fitRect: CGRect = AVMakeRect(aspectRatio: baseSZ, insideRect: maxRect)
            
            // "scaled" font size
            let newPointSize: CGFloat = fitRect.height / baseSZ.height * baseFont.pointSize
            
            // create a new font at calculated point size
            guard let newFont: UIFont = UIFont(name: baseFont.fontName, size: newPointSize) else { return }
            
            let newIMG: UIImage = drawTextOnImage(theString, withFont: newFont, inRect: fitRect, textColor: .yellow, onImage: theImage)
            
            // update the image view
            imgView.image = newIMG
            
        }
        
        func drawTextOnImage(_ str: String, withFont: UIFont, inRect: CGRect, textColor: UIColor, onImage: UIImage) -> UIImage {
            
            let imgSZ: CGSize = CGSize(width: onImage.size.width, height: onImage.size.height)
            let renderer = UIGraphicsImageRenderer(size: imgSZ)
            
            let img = renderer.image { ctx in
                
                // draw the image
                onImage.draw(at: .zero)
                
                // centered text
                let paragraphStyle = NSMutableParagraphStyle()
                paragraphStyle.alignment = .center
                
                let attrs: [NSAttributedString.Key : Any] = [
                    .font: withFont,
                    .foregroundColor: textColor,
                    .paragraphStyle: paragraphStyle,
                ]
                
                // draw the string
                str.draw(with: inRect, options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
            }
            
            return img
            
        }
        
    }
    

    We can enter an image name to load, or generate solid-blue images with the entered dimensions, and draw the string at the calculated font size:

    enter image description here

    enter image description here enter image description here

    enter image description here enter image description here

    Note: this is a starting point. You’ll notice that the calculated text rect and position is based on the font, not the specific characters.

    So, we can get this:

    enter image description here enter image description here

    If you need the text vertically centered based on the actual characters, you’ll want to calculate the glyph bounding box and offset the drawing.


    Editforgot to include the sizeOfString font extension…

    extension UIFont {
        func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
            let attributes = [NSAttributedString.Key.font:self]
            let attString = NSAttributedString(string: string,attributes: attributes)
            let framesetter = CTFramesetterCreateWithAttributedString(attString)
            return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0,length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search