How to get *real* height of CFAttributedString - swift

I create a simple CFAttributedString with Futura-Bold typeface and size 100px:
let font = NSFont(name: "Futura-Bold", size: 100.0)!
When I render that string on a CGContext (with CTFramesetterCreateFrame) I get the following image:
Now the question is how to get real height of this text? As you can see with the above example we're looking at 85px.
When querying font object for various attributes I get the following values:
font.pointSize // 100.0
font.ascender // 103.90
font.descender // -25.99
font.capHeight // 75.40
font.leading // 2.99
font.boundingRectForFont // (-22.7, -34.3994140625, 168.6, 144.29931640625)
Does anyone know to calculate real pixel size of rendered string?

One solution that gives you the value you are seeking is to use the NSString boundingRect(with:options:attributes:) method. By passing in the proper options you get the desired result:
let font = NSFont(name: "Futura-Bold", size: 100)!
let text: NSString = "Hello World!"
let rect = text.boundingRect(with: NSSize(width: 0, height: 0), options: [ .usesDeviceMetrics ], attributes: [ .font: font ], context: nil)
print("Height of \"\(text)\" is \(rect.height)")
Output:
Height of "Hello World!" is 85.1
This works with NSAttributedString as well.
let font = NSFont(name: "Futura-Bold", size: 100)!
let attrStr = NSAttributedString(string: "Hello World!", attributes: [ .font: font ])
let rect2 = attrStr.boundingRect(with: NSSize(width: 0, height: 0), options: [ .usesDeviceMetrics ])
print("Height of \"\(attrStr)\" is \(rect2.height)")
Output:
Height of "Hello World!{
NSFont = "\"Futura-Bold 100.00 pt. P [] (0x7ff6eae563b0) fobj=0x7ff6eaf1ea50, spc=34.00\"";
}" is 85.1
If needed, you can cast a CFAttributedString to NSAttributedString.
let attrStr: CFAttributedString = ... // some CFAttributedString
let rect2 = (attrStr as NSAttributedString).boundingRect(with: NSSize(width: 0, height: 0), options: [ .usesDeviceMetrics ])

In addition to rmaddys's excellent answer I found one more solution on my own which also gives desired results. The trick is to use CTLineGetImageBounds.
let font = NSFont(name: "Futura-Bold", size: 100.0)!
let text = NSAttributedString(string: "Hello World!", attributes: [.font:font])
let line = CTLineCreateWithAttributedString(text)
print(CTLineGetImageBounds(line, textContext))
where textContext is a CGContext on which you render your text. Per Apple docs:
This is required because the context could have settings in it that
would cause changes in the image bounds.
The above code gives the following result:
(7.9, -2.1, 664.2, 85.1)
^^^^

Related

NSFont autoscaling via NSStringDrawingContext does not work

I'm building a small test app that draws text into rectangles on images. Said text might sometimes be too long to be drawn into said rectangles, but I thought I would simply use NSStringDrawingContext and NSAttributedString.boundingRect(with:options:context:) in order to generate a scale factor that I'd use to scale the font down to make it fit.
The problem I'm now facing is that this straight up is not working, as actualScaleFactor is always 1.0, even though it should be less than that. To demonstrate, I've prepared a minimal reproducible example.
The image used as input variable image can be found here: https://i.imgur.com/VNd2y8r.png. The text target rectangle rect is marked by the black rectangle in the image.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes: [NSAttributedString.Key: Any] = [
.font: NSFont(name: "Futura-Bold", size: 100)!,
.foregroundColor: NSColor.white,
.paragraphStyle: paragraphStyle,
]
let text = "A very long string that'll be auto-wrapped when it gets drawn"
let attrStr = NSAttributedString(string: text, attributes: textAttributes)
let drawingOptions: NSString.DrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
let context = NSStringDrawingContext()
// counter-intuitively, 1.0 is the "maximum" minimum scale factor
// see also https://developer.apple.com/documentation/uikit/nsstringdrawingcontext/1534020-minimumscalefactor
context.minimumScaleFactor = 1.0
let position = NSPoint(x: 80, y: 140)
let size = NSSize(width: 640, height: 200)
let rect = NSRect(origin: position, size: size)
let newImage = NSImage(size: image.size, flipped: true) { imageRect in
image.draw(in: imageRect)
// let NSStringDrawingContext do its thing
attrStr.boundingRect(with: size, options: drawingOptions, context: context)
print("Actual Scale Factor: \(context.actualScaleFactor)") // is always 1.0
attrStr.setAttributes([.font: font.withSize(font.pointSize * context.actualScaleFactor)], range: NSRangeFromString(text))
attrStr.draw(with: rect, options: drawingOptions, context: context)
return true
}
At this point, newImage should be an image that looks something like this:
(recreated in an image editing tool)
Instead, this is what I get:
This is obviously wrong.
While I was looking for a solution, I found this related SO question from 2013 with a conclusion of "it's broken", but surely this has to be possible somehow by now. iOS and macOS label controls both have this feature, SwiftUI even makes it available as a modifier entitled simply minimumScaleFactor. Is there a replacement API I'm not aware of?

Discrepancy when calculating geometrical size of string

Why are the results of NSAttributedString.size() and NSLayoutManager.boundingRect(forGlyphRange:in:).size so different?
Example code:
let attrs: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: 14.0)]
let container = NSTextContainer()
container.lineFragmentPadding = 0.0
container.lineBreakMode = .byClipping
container.containerSize = NSSize(width: 5e6, height: 5e6)
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(container)
let text = NSTextStorage()
text.addLayoutManager(layoutManager)
text.setAttributedString(NSAttributedString(string: "Hello, world!", attributes: attrs))
var range = NSMakeRange(0, text.length)
print(text.size())
print(layoutManager.boundingRect(forGlyphRange: range, in: container).size)
text.setAttributedString(NSAttributedString(string: "東京", attributes: attrs))
range = NSMakeRange(0, text.length)
print(text.size())
print(layoutManager.boundingRect(forGlyphRange: range, in: container).size)
Here's the result I get:
(80.3837890625, 18.0)
(80.3837890625, 17.0)
(25.85410895660203, 14.0)
(25.85410895660203, 19.463557481765747)
As you can see, although the widths are the same, the heights are not. In the first case the NSLayoutManager reports a smaller height than NSAttributedString, and in the second case a larger one.
What accounts for this difference?
I have read the documentation for these two methods. I have also read the Text System Storage Layer Overview in Apple's archive. I could not find the information there.

Swift: Can I adjust attributed text with dynamic font size?

I'm working with the collectionView
whose each cell has the content like this:
everything works pretty well until I test it on an iphone 5s:
"Ho Chi Minh City" was out of bounds.
Here is my code:
func configureNameLabel() {
nameLabel.numberOfLines = 2
let attributedText = NSMutableAttributedString(string: post?.name ?? "", attributes: [.font: UIFont.boldSystemFont(ofSize: 15)])
attributedText.append(NSAttributedString(string: "\nSaturday, December 1, 2018 ⦁ Ho Chi Minh City ⦁ ", attributes:
[.foregroundColor: UIColor.lightGray, .font: UIFont.preferredFont(forTextStyle: .caption1)]))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5
attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedText.string.count))
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "user-male")
attachment.bounds = CGRect(x: 0, y: -2, width: 12, height: 12)
attributedText.append(NSAttributedString(attachment: attachment))
nameLabel.attributedText = attributedText
}
I think this problem is due to the font size, can anybody fix this ?
Thanks guys, I solved it. "label.numberOfLines = 0" worked. the problem is at my constraints.

How to get the height of a UILabel in Swift?

I am a beginner in Swift and I am trying to get the height of a label.
The label has multiple lines of text. I want to know the total height it occupies on the screen.
Swift 4 with extension
extension UILabel{
public var requiredHeight: CGFloat {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.attributedText = attributedText
label.sizeToFit()
return label.frame.height
}
}
it's simple, just call
label.bounds.size.height
Updated for Swift 3
func estimatedHeightOfLabel(text: String) -> CGFloat {
let size = CGSize(width: view.frame.width - 16, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSFontAttributeName: UIFont.systemFont(ofSize: 10)]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
override func viewDidLoad() {
super.viewDidLoad()
guard let labelText = label1.text else { return }
let height = estimatedHeightOfLabel(text: labelText)
print(height)
}
Swift 5 ioS 13.2 tested 100%, best solution when the UILabel numberOfLines = 0
Note, result is rounded. Just remove ceil() if you don't want it.
If you want to get height -> give storyboard width of UILabel
If you want to get width -> give storyboard height of UILabel
let stringValue = ""//your label text
let width:CGFloat = 0//storybord width of UILabel
let height:CGFloat = 0//storyboard height of UILabel
let font = UIFont(name: "HelveticaNeue-Bold", size: 18)//font type and size
func getLableHeightRuntime() -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = stringValue.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
func getLabelWidthRuntime() -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = stringValue.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width)
}
#iajmeri43's answer Updated for Swift 5
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
To use it:
// 1. get the text from the label
guard let theLabelsText = myLabel.text else { return }
// 2. get the width of the view the label is in for example a cell
// Here I'm just stating that the cell is the same exact width of whatever the collection's width is which is usually based on the width of the view that collectionView is in
let widthOfCell = self.collectionView.frame.width
// 3. get the font that your using for the label. For this example the label's font is UIFont.systemFont(ofSize: 17)
let theLabelsFont = UIFont.systemFont(ofSize: 17)
// 4. Plug the 3 values from above into the function
let totalLabelHeight = estimatedLabelHeight(text: theLabelsText, width: widthOfCell, font: theLabelsFont)
// 5. Print out the label's height with decimal values eg. 95.46875
print(totalLabelHeight)
// 6. as #slashburn suggested in the comments, use the ceil() function to round out the totalLabelHeight
let ceilHeight = ceil(totalLabelHeight)
// 7. Print out the ceilHeight rounded off eg. 95.0
print(ceilHeight)

How to make text appear in centre of the image?

I want to add a text on an image, and I'm able to do it as shown in below code.
func textToImage(drawText: NSString, inImage: UIImage, atPoint:CGPoint)->UIImage{
// Setup the font specific variables
var textColor: UIColor = UIColor.redColor()
var textFont: UIFont = UIFont(name: "AmericanTypewriter", size: 75)!
let textStyle = NSTextEffectLetterpressStyle
//Setup the image context using the passed image.
UIGraphicsBeginImageContext(inImage.size)
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSTextEffectAttributeName : textStyle
]
inImage.drawInRect(CGRectMake(0, 0, inImage.size.width, inImage.size.height))
var rect: CGRect = CGRectMake(atPoint.x, atPoint.y, inImage.size.width, inImage.size.height)
drawText.drawInRect(rect, withAttributes: textFontAttributes)
var newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
I called this function like shown below
let newImage : UIImage = textToImage(textToAdd!, inImage: imageToSave, atPoint:CGPoint(x: 20, y: 20)) //For now text is displaying at the top in left end for point(x:20 ,y:20)
But the problem is, text is taken by user. They can give either single word or big sentence. So if they give only single word, it should appear in top and middle of the image or else in top from left to right. it is more like, I want to set the text alignment to centre. To achieve it, Accordingly I should change the CGPoint given above. But I'm not getting how to change it so that it should look good in all scenarios. Please help me how to achieve this.
You can center the text near the top of the image like this:
func textToImage(drawText: NSString, inImage: UIImage, atPoint:CGPoint)->UIImage{
// Setup the font specific variables
var textColor: UIColor = UIColor.redColor()
var textFont: UIFont = UIFont(name: "AmericanTypewriter", size: 75)!
let textStyle = NSTextEffectLetterpressStyle
//Setup the image context using the passed image.
UIGraphicsBeginImageContext(inImage.size)
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSTextEffectAttributeName : textStyle
]
inImage.drawInRect(CGRectMake(0, 0, inImage.size.width, inImage.size.height))
let textSize = drawText.sizeWithAttributes(textFontAttributes)
let textRect = CGRectMake(inImage.size.width / 2 - textSize.width / 2, 0,
inImage.size.width / 2 + textSize.width / 2, inImage.size.height - textSize.height)
drawText.drawInRect(textRect, withAttributes: textFontAttributes)
var newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
Added to #osteven answer. Below code works as I expected. Hope it helps others.
func textToImage(drawText: NSString, inImage: UIImage, atPoint:CGPoint)->UIImage{
// Setup the font specific variables
var textColor: UIColor = UIColor.redColor()
var textFont: UIFont = UIFont(name: "AmericanTypewriter", size: 75)!
let textStyle = NSTextEffectLetterpressStyle
//Setup the image context using the passed image.
UIGraphicsBeginImageContext(inImage.size)
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSTextEffectAttributeName : textStyle
]
inImage.drawInRect(CGRectMake(0, 0, inImage.size.width, inImage.size.height))
let textSize = drawText.sizeWithAttributes(textFontAttributes)
if textSize.width < inImage.size.width {
println("lesser")
let textRect = CGRectMake(inImage.size.width / 2 - textSize.width / 2, 0,
inImage.size.width / 2 + textSize.width / 2, inImage.size.height - textSize.height)
drawText.drawInRect(textRect, withAttributes: textFontAttributes)
}
else {
println("greater")
let textRect = CGRectMake(0 , 0,
inImage.size.width, inImage.size.height)
drawText.drawInRect(textRect, withAttributes: textFontAttributes)
}
var newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
It may be a simple as what you suggested with "I want to set the text alignment to centre".
Add a line here:
// Setup the font specific variables
var textColor: UIColor = UIColor.redColor()
var textFont: UIFont = UIFont(name: "AmericanTypewriter", size: 75)!
let textStyle = NSTextEffectLetterpressStyle
let textAlignment = NSTextAlignment.Center
and add to your array:
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSTextEffectAttributeName : textStyle
NSTextAlignment: textAlignment
]
This should make the text centered in the image.
If you change the point of text origin back to (0, 0) and set your Text Rectangle as big as the image, it should be able to account for the width of Rect and thus center all of the text accordingly.