3

I would like to apply a single solid line stroke to text. This is easily obtained using NSAttributedString specifying the .strokeWidth. However I am finding it to be tricky to determine what the strokeWidth should be given a UIFont to be rendered at any given pointSize. I can easily say, okay at a point size of 50 a stroke width of 1 looks great. I intuitively assumed if you double the font size you should double the stroke width, thus as the font scales the stroke will scale proportionally and result in the stroke thickness appearing consistent with the original "base" font size. However this is not what occurs. As the font size and stroke width increase proportionally, the stroke width becomes much too thick.

enter image description here

The screenshot here shows the first line is a font size of 50 and stroke width of 1. Next line is doubled so font size 100 stroke width 2, and this is repeated until the last line which is 350 vs 7.

I believe this is occurring because the stroke is rendered both inward and outward. Its center is at the edge of the character, then it expands in both directions. You can see this if you compare these two images, this one without a stroke applied.

enter image description here

So as the font size increases, the stroke width should not increase proportionally, it needs to increase at a slower rate to ensure the thickness is consistent across all the sizes. I am trying to determine the correct way to calculate this value.

So given a base configuration that looks desirable (let's just say 50pt font size and 1pt stroke width) and a new pointSize (for example 350pt), how do you calculate the correct strokeWidth? Or perhaps I should utilize a different value not pointSize?

My current algorithm that proportionally scales it is:
let strokeWidth = font.pointSize / 50 (simply solving for x in 1/50 = x/pointSize)

Here is the code I'm using to draw this text:

    let text = "hello"
    let imageRect = CGRect(x: 0, y: 0, width: 343 * 3, height: 500 * 3)

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let alphaInfo = CGImageAlphaInfo.premultipliedLast.rawValue

    let bitmapContext = CGContext(data: nil, width: Int(imageRect.width), height: Int(imageRect.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: alphaInfo)!
    bitmapContext.setAlpha(1)
    bitmapContext.setTextDrawingMode(CGTextDrawingMode.fill)

    //1
    bitmapContext.textPosition = CGPoint(x: 40, y: 1080)
    let displayLineText1 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 50), .strokeColor: UIColor.red, .strokeWidth: 1]))
    CTLineDraw(displayLineText1, bitmapContext)

    //2
    bitmapContext.textPosition = CGPoint(x: 40, y: 1000)
    let displayLineText2 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 100), .strokeColor: UIColor.red, .strokeWidth: 2]))
    CTLineDraw(displayLineText2, bitmapContext)

    //3
    bitmapContext.textPosition = CGPoint(x: 40, y: 875)
    let displayLineText3 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 150), .strokeColor: UIColor.red, .strokeWidth: 3]))
    CTLineDraw(displayLineText3, bitmapContext)

    //4
    bitmapContext.textPosition = CGPoint(x: 40, y: 725)
    let displayLineText4 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 200), .strokeColor: UIColor.red, .strokeWidth: 4]))
    CTLineDraw(displayLineText4, bitmapContext)

    //5
    bitmapContext.textPosition = CGPoint(x: 40, y: 540)
    let displayLineText5 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 250), .strokeColor: UIColor.red, .strokeWidth: 5]))
    CTLineDraw(displayLineText5, bitmapContext)

    //6
    bitmapContext.textPosition = CGPoint(x: 40, y: 310)
    let displayLineText6 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 300), .strokeColor: UIColor.red, .strokeWidth: 6]))
    CTLineDraw(displayLineText6, bitmapContext)

    //7
    bitmapContext.textPosition = CGPoint(x: 40, y: 40)
    let displayLineText7 = CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: [.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 350), .strokeColor: UIColor.red, .strokeWidth: 7]))
    CTLineDraw(displayLineText7, bitmapContext)

    let textCGImage = bitmapContext.makeImage()!
    let textImage = CIImage(cgImage: textCGImage)
Jordan H
  • 52,571
  • 37
  • 201
  • 351
  • Probably not answer as it would be don’t dependent. You would probably need some sort do glyph thickness. While you can some of this from Core Text you probably cannot get the thickness of the stroke just the frame and position. – agibson007 Dec 16 '18 at 02:03

1 Answers1

0

From the documentation for .strokeWidth (emphasis added):

The value of this attribute is an NSNumber object containing a floating-point value. This value represents the amount to change the stroke width and is specified as a percentage of the font point size. Specify 0 (the default) for no additional changes. Specify positive values to change the stroke width alone. Specify negative values to stroke and fill the text. For example, a typical value for outlined text would be 3.0.

So, the value is already scaled to the font size. You should not also be scaling it yourself. Pick a value which gives the relative stroke width you like at any given font size and use that for all font sizes.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154