skip to Main Content

Using CoreGraphics to render text with attributes as as an image and return that image – the code that returns the image is:

 func textualImage(text: String, textSize: CGFloat, textColor: UIColor, backgroundColor: UIColor = .white) -> UIImage? {
    
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.alignment = .center
    
    let attributes = [
        NSAttributedString.Key.foregroundColor: textColor,
        NSAttributedString.Key.font: UIFont.systemFont(ofSize: textSize),
        NSAttributedString.Key.backgroundColor: UIColor.white,
        NSAttributedString.Key.paragraphStyle: paragraphStyle
    ]
    
    let textSize = text.size(withAttributes: attributes)
    
    UIGraphicsBeginImageContextWithOptions(textSize, true, 0)
    
    backgroundColor.setFill()  // tried this but no different
    let rect = CGRect(origin: .zero, size: textSize)
    text.draw(in: rect, withAttributes: attributes)
    
    let imageOriginal = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return imageOriginal
}

When running this code, I will get black lines (sometimes thin, sometimes thick, sometimes any edge) like in this photo

showing the black line on the bottom, and faintly on the trailing edge

Have tried providing a background color and using the .setFill() function on the color inside of the graphics rendering context. Would anyone be able to help me understand why this is happening?

This is being done with text annotations to ‘flatten’ that particular field of a PDF so it can no longer be altered.

To note, if the text provided is multiple lines and one line is shorter than another, whatever space is the ’empty overlap’ is solid black – this seems to be relevant to the issue but changing the size of the graphics context is making no difference.

Any help would be appreciated thank you

2

Answers


  1. Chosen as BEST ANSWER

    It appears the solution to this is two parts. Using the method in the question was forcing the black lines because that method colors the entire background black and then the text had the background of white. While there seems to be a solution of coloring the background of the context to other-than-black, I could not get the method to work. Instead I changed the rendering method itself to what is below, which gives no background color to the rendered area by default and removing the background color of the text then draws only the text in the frame, with no other background. The text background removal (as suggested by Cy-4AH) with the original style produces a solid black box, but removing the background with this style renders as expected.

       func image(textSize: CGFloat, textColor: UIColor) -> UIImage? {
        
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        
        let attributes = [
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: textSize),
            NSAttributedString.Key.backgroundColor: UIColor.clear,
            NSAttributedString.Key.paragraphStyle: paragraphStyle
        ]
        
        let textSize = self.size(withAttributes: attributes)
        
        let renderer = UIGraphicsImageRenderer(size: textSize)
        
        let image = renderer.image { context in
            let rect = CGRect(origin: .zero, size: textSize)
            self.draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
        }
        
        return image
    }
    

  2. You initial method is quite OK and is working exactly as expected. Let’s break it down to more simple components.

    First of all try to just remove the text part and get the image from the context itself:

    UIGraphicsBeginImageContextWithOptions(textSize, true, 0)
    let imageOriginal = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()    
    return imageOriginal
    

    The result is a black rectangle. And the reason for it is because a default color in a context is a transparent color as RGBA (0,0,0,0). But in your case you set a context to be opaque which means it is translated to RGB (0,0,0), a black color.

    Simply setting you options as UIGraphicsBeginImageContextWithOptions(textSize, false, 0) the black rectangle is gone.

    The opaque value may still make sense but in that case you should draw some color over the whole context with paths. For instance

    UIGraphicsBeginImageContextWithOptions(textSize, true, 0)
    
    backgroundColor.setFill()
    let rect = CGRect(origin: .zero, size: textSize)
    UIBezierPath(rect: rect).fill()
    
    let imageOriginal = UIGraphicsGetImageFromCurrentImageContext()
    

    This is how the context rect is being set to a certain color. And this specific code isolates your issue that you were initially facing. You can see anomalies on right or/and bottom side of your rectangle. The reason for this is that your text size has a "strange" value and is not an integer. Both, context and returned image, will have integer size. So you need to round those values.

    You can get your context size by using

    guard let context = UIGraphicsGetCurrentContext() else { return nil }
    let contextSize = CGSize(width: context.width, height: context.height)
    

    But you will find out that the size of a context is much larger as it already includes your screen scale. This has happened again because of the options you set when generating your context. By providing a scale of 0 you let system to decide the scale of your context. That is OK in most cases but in your case you may wish to control this based on the scale of your PDF or your screen (From your question it is hard to tell). Try it like so:

    let textSize = text.size(withAttributes: attributes)
    let scale = UIScreen.main.scale
    let canvasSize = CGSize(width: ceil(textSize.width*scale),
                             height: ceil(textSize.height*scale))
    let contextSize = CGSize(width: canvasSize.width/scale,
                             height: canvasSize.height/scale)
    
    UIGraphicsBeginImageContextWithOptions(contextSize, true, scale)
    

    So now that we have all values we can try and put it all together:

    func textualImage(text: String, textSize: CGFloat, textColor: UIColor, backgroundColor: UIColor = .white, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
        
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
    
        let attributes = [
            NSAttributedString.Key.foregroundColor: textColor,
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: textSize),
    //        NSAttributedString.Key.backgroundColor: UIColor.white, // No need for this
            NSAttributedString.Key.paragraphStyle: paragraphStyle
        ]
        
        let textSize = text.size(withAttributes: attributes) // Estimated size of text
        let canvasSize = CGSize(width: ceil(textSize.width*scale),
                                 height: ceil(textSize.height*scale)) // Minimum size in pixels to fit the text
        let contextSize = CGSize(width: canvasSize.width/scale,
                                 height: canvasSize.height/scale) // Actual context size
        
        // Generate a new context
        UIGraphicsBeginImageContextWithOptions(contextSize, true, scale)
        
        // Fill background with given color:
        backgroundColor.setFill() // Will set a fill color to current context
        let contextRect = CGRect(origin: .zero, size: contextSize)
        UIBezierPath(rect: contextRect).fill()
        
        // Draw text
        let textRect = CGRect(origin: .zero, size: textSize)
        text.draw(in: textRect, withAttributes: attributes)
        
        // Extract the image
        let imageOriginal = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return imageOriginal
    }
    

    I hope the comments give enough additional information.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search