8

I need to create an image containing one line of text. But the problem, i first need to create the context (CGBitmapContextCreate) with require the width and the height of the image to have the possibility to later calculate the bounds of the text (via CTLineGetImageBounds). but i want the size of the image = the bounds of the text :( how can i do ?

actually i use

CGBitmapContextCreate
CTLineGetImageBounds
CTLineDraw

maybe it's possible to call CTLineGetImageBounds without a context ?

Note: i m on delphi, it's not really a problem as i can have access to all the API, i just need the function name

zeus
  • 12,173
  • 9
  • 63
  • 184

6 Answers6

6

You can calculate the space an NSString will take up based on the font you want to use by doing the following:

NSString *testString = @"A test string";
CGSize boundingSize = self.bounds.size;
CGRect labelRect = [testString
                    boundingRectWithSize:boundingSize
                    options:NSStringDrawingUsesLineFragmentOrigin
                    attributes:@{ 
                        NSFontAttributeName : [UIFont systemFontOfSize:14]
                                }
                    context:nil];

Where bounding size is the maximum size you want the image to be. Then you can use the calculated size to create your image.

Ashley
  • 156
  • 5
  • not understand, because i have only access to api like CGBitmapContextCreate, CTLineGetImageBounds, etc.. i don't understand with function you call here ? CGRect labelRect = [testString boundingRectWithSize:boundingSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:14] } context:nil]; – zeus Jun 08 '16 at 13:28
  • If you add #import to your file, you will have access to NSString and its APIs. – Ashley Jun 08 '16 at 16:40
  • but i don't understand, with function name is this : [testString boundingRectWithSize:boundingSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:14] } context:nil] ... i work from Delphi and i need a api function name :( – zeus Jun 09 '16 at 00:06
  • The method's name is `[NSString boundingRectWithSize:options:attributes:]` (ObjC syntax) or `NSString.boundingRectWithSize(_:options:attributes:)` (Swift syntax). If Delphi can access all of Cocoa, then it should be able to access it. The functions you're describing in your question are part of a low-level C API that most Cocoa devs never use. They're fine for this purpose, but if you are having trouble reading ObjC method names, you're going to have a lot of trouble building anything non-trivial. Many features have no C API. – Rob Napier Jun 10 '16 at 14:34
  • unfortunately it's look like i don't have boundingRectWithSize in delphi :( maybe i have access only to low level C API ... – zeus Jun 10 '16 at 21:01
  • Are you using "uses Macapi.CoreFoundation;" to import Core Foundation? If so, add "Macapi.Foundation" to get access to NSString. – robinkunde Jun 16 '16 at 21:11
5

This the could you need to write in order to get the size of the text with the font.

let widthOfLabel = 400.0
let size = font?.sizeOfString(self.viewCenter.text!, constrainedToWidth: Double(widthOfLabel))

You have to use the below extension of the font in order to get the size of the text with the font.

Swift 5:

extension UIFont {
  func sizeOfString(string: String, constrainedToWidth width: CGFloat) -> CGSize {
    return NSString(string: string).boundingRect(
      with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
      options: NSStringDrawingOptions.usesLineFragmentOrigin,
      attributes: [NSAttributedString.Key.font: self],
      context: nil
    ).size
  }
}

Former Swift 2 code:

extension UIFont {
    func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
        return NSString(string: string).boundingRectWithSize(CGSize(width: width, height: DBL_MAX),
                                                             options: NSStringDrawingOptions.UsesLineFragmentOrigin,
                                                             attributes: [NSFontAttributeName: self],
                                                             context: nil).size
    }
}
Tomáš Kafka
  • 4,405
  • 6
  • 39
  • 52
3

There are a couple of ways to calculate a more or less precise bounding box. You can use font metrics, iterate CTRuns or use cocoa's string drawing API for calculation.

Yet, I want to draw your attention to the problem that this is not generally possible. While the results from above algorithms are sufficient in many cases, there are a couple of scenarios where they are utterly wrong:

  • some letters have parts that reach very far out of the metrics defined in the font. Here's an example from Zapfino: a variation of lower case "f". (Green is Text Edit's selection)

Zapfino lower case f variation.

  • There are effects that reach out of the calculated box. Outline and shadow are just examples. Shadows can have a very far distance and huge blur.

enter image description here

  • There might be custom text attributes that your algorithm is not aware of.

  • A text can contain "attachments" (like images, I think emoji are also implemented as attachments). Those have to be taken into account as well.

When I had to solve this problem before, I finally reached the following solution:

  1. Create a transparent bitmap context at maximum size (in this case a full page).
  2. Draw the text at the desired position.
  3. Walk over the bytes that back the context from outside to inside, scanning the first non-transparent pixel along each edge.
  4. Cut the resulting rectangle using CGImageCreateWithImageInRect.

That's utterly slow, but speed was no priority for my use case.

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • thanks a lot Nikolai, yes it's a way but this will be terribly slow :( and in my case speed is very important :( – zeus Jun 10 '16 at 20:39
2

OK, i found the solution: use CTLineGetImageBounds(aline, null{context}); simply pass null for the context and it's work as expected ;)

zeus
  • 12,173
  • 9
  • 63
  • 184
  • Hm, that seems to be not backed by the documentation: "This is required because the context could have settings in it that would cause changes in the image bounds.". Do you have another reference? – Nikolai Ruhe Jun 13 '16 at 06:19
1

Below code using to calculate the NSString Size based on the font

extension UIFont {
func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
    return NSString(string: string).boundingRectWithSize(CGSize(width: width, height: DBL_MAX),
        options: NSStringDrawingOptions.UsesLineFragmentOrigin,
        attributes: [NSFontAttributeName: self],
        context: nil).size
}
}

OR Alternatively you could cast it into an NSString

if let ns_str:NSString = str as NSString? {

let sizeOfString = ns_str.boundingRectWithSize(
                             CGSizeMake(self.titleLabel.frame.size.width, CGFloat.infinity), 
                             options: NSStringDrawingOptions.UsesLineFragmentOrigin, 
                             attributes: [NSFontAttributeName: lbl.font], 
                             context: nil).size
}
HariKrishnan.P
  • 1,204
  • 13
  • 23
1

It sounds like you've already done most of the work. You just need to create a temporary context to call CTLineGetImageBounds against. I believe it's ok for that context to be very small (one pixel, for instance, maybe even zero), and of course you can reuse it. Ideally it should have the same attributes (other than size) as your final context, though even that probably won't matter in most cases. It's only required for its metadata.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • yes i was thinking about creating a dummy context, but i don't know if the operation to create a dummy context (with size=0) is expensive of CPU cycles or not. So the idea will be to create a dummy context, calculate the size, free the dummy context, create a context with the calculated size and paint on it. Ideally i would like to keep the dummy context in memory during all the life of the app, but i not sure if CTLineGetImageBounds work in multithread :( – zeus Jun 10 '16 at 20:57