Getting a glyph boundingRect in draw#rect in UILabel - ios

Using Swift, I want to get the boundingRect of a glyph, in draw#rect in a UILabel.
The UILabel already has a size (say 300x300 in the example) and qualities such as the text being centered.
class RNDLabel: UILabel {
override func draw(_ rect: CGRect) {
let manager = NSLayoutManager()
let store = NSTextStorage(attributedString: NSAttributedString(
string: text!,
attributes: [NSAttributedString.Key.font: font]))
store.addLayoutManager(manager)
let textContainer = NSTextContainer(size: rect.size)
// note, intrinsicContentSize is identical there, no difference
manager.addTextContainer(textContainer)
let glyphRange = manager.glyphRange(
forCharacterRange: NSRange(location: 0, length: 1),
actualCharacterRange: nil)
let glyphRect = manager.boundingRect(
forGlyphRange: glyphRange, in: textContainer)
print("glyphRect \(glyphRect)")
...context?.addRect(glyphRect), context?.drawPath(using: .stroke)
super.draw(rect)
}
The green square is not correct - it should be more like these red squares!
There seems to be a number of problems:
surely the layoutManager I make, should get the qualities of the UILabel, eg "centered text"? (I believe you can't actually directly access the layoutManager of a UILabel, though?)
should we be using something like CTLineGetOffsetForStringIndex? Is that even possible in draw#rect
notice as well as not having the correct offset, the green box seems the wrong height anyway. (It looks more like a plain old intrinsicContentSize rather than a glyph bounding box.)
How to?
For the record, my overall aim is to move the glyph around, based on the actual glyph box (which of course is different for "y", "X", etc). But in general there are many useful reasons to know the box of a glyph.

Your original question is sort of an unquestion, because it says two completely opposite things.
On the one hand, you say UILabel.
On the other hand, you use terms like NSLayoutManager and NSTextStorage and NSTextContainer — the TextKit stack.
Those are opposites because UILabel is not drawn with the TextKit stack. So what you're asking to do is like trying to lift a piece of paper with a magnet; magnets do lift things, but what they lift are magnetic materials, and paper is not one of those. To be sure, UILabel must draw in some deterministic way, but what that way is, is completely unknown and opaque and has nothing to do with TextKit.
On the other hand, UITextView, or a view that your draw yourself using TextKit, does use TextKit, and now all the TextKit tools apply. So what you're asking to do would be a lot more straightforward if this were a UITextView (or a view that you draw yourself using TextKit).
This need not change anything very much about the appearance of the interface. A UITextView can be made to act a lot like a UILabel (make it noneditable and nonscrollable) but with the important difference that the whole text kit stack is directly exposed. So that's where I'd start. I'm one of those people who prefers to use the framework rather than to fight it.

It turns out the answer to this question seems to be:
In fact, surprisingly or not, you basically have no access to glyph information in UILabel specifically.
So realistically you can't get those glyph "actual shapes" in UILabel.
In particular RobN. has pointed out that an investigation showed that in UILabel, _drawTextInRect:baselineCalculationOnly does the work and that is a big pile of ad hoc code.
A summary of the situation would seem to be that UILabel simply predates NSLayoutManager / Core Text and (as yet) just plain does not use those modern systems.
Only those modern systems give you this ...
... sort of access to glyph-by-glyph shapes.

Related

iOS UILabel and avoiding clipping of diacritics with custom font

First of all, there are many questions on StackOverflow, but none that fully answer this question.
The problem is mainly, but most likely not limited to, Thai and Arabic diacritics when rendered with a custom Latin-only font, using the text property of a UILabel. Which is also intrinsically sized in an auto-layout. I've already done everything Apple suggests, playing with the settings mentioned in their documentation, WWDC videos, as well as questions on StackOverflow (e.g. clipsToBounds = NO, etc.). Keep in mind, only my custom font setup clips in my scenario, not the iOS system font (.SF-UIDisplay), and not even the iOS system provided Helvetica or Helvetic Neue. The custom font has been checked and rechecked, and at this point the conclusion, iOS is the anomaly across all platforms, even macOS. To be even clearer, the same clipping behavior as the custom font can be seen with SF Pro, a font provided by Apple themselves here: https://developer.apple.com/fonts/
This question is about the most proper, least intrusive, and most complete way to do what is necessary to not clip diacritics. Meaning, how would you do this, ideally, from scratch.
All of my font research and test runs have led all those involved in this problem to believe that Apple has implemented special treatment specifically for their system fonts in UILabel, to avoid diacritic clipping. So making that an assumption, I'm also assuming the font is ok, and I'm looking for solutions that do not involve editing the font.
In my tries to use the font, the first thing to go wrong was vertical clipping of the ascender diacritics of Thai glyphs:
นื้ทั้มูHello
This means the glyphs of the font Thonburi when they cascade from the custom Latin-only font. The fix from this point, was to use a custom font only for Thai without any Latin characters, so it could be defined as the primary font, and cascade to the previously mentioned Latin-only custom font. After all this, the custom Thai font still has horizontal clipping issues on diacritics that come at the end of the text:
Worldฟล์
So now I am at a loss for anything further that font management puppetry can do (though still open to suggestions), and I am moving on to more code-centric fixes. I've seen quite a few questions and answers mentioning subclassing UILabel, but I'd like to know what this would look like that could accomplish what I've described.
I'd also like to know if just opting out of UILabel would be an option for anyone. Meaning would writing something from the ground up with TextKit be worth it to avoid all these bugs that seem to only plague iOS, and specifically UILabel.
At first I thought this was a problem with the framework but it's not, it's just a strict enforcement of a font's metrics. And in probably everything but web/app development, fonts are not rendered so strictly, which is why this problem rarely comes up. Fonts have a number of metrics that tell the program rendering it onto the screen how to render it, most importantly how to handle padding. And UILabel (and UITextField, and likely others) applies these metrics strictly. And the problem for us is that some fonts are very decorative and are often too thick or oblique to fit perfectly into the square canvas that each character must fit into (this is especially the case with accents, like umlauts). This isn't a problem outside of web/app development because when a character doesn't fit into its canvas, like a very thick, wide, and oblique W, the program just shows it anyway, and that's why a low-hanging g might spill into the line below it. But if that g was rendered in a single-line UILabel, because of how strict the font-metric enforcement is in iOS, that low-handing g is clipped.
Subclassing UILabel (in the case of UILabel) and overriding its intrinsicContentSize to add some extra padding is not a good idea, on further research. For one, it's kind of hacky, but more importantly, it produces constraint warnings in the debugger. The true fix, and the only acceptable fix AFAIK, is to edit the font's metrics.
Download a program like Glyphs (https://glyphsapp.com/), open the font, open the Font's Info, and in the Masters tab, give the font the proper ascender and descender values. To best understand how these values work, open the San Francisco font in the program and see how Apple did it (it's the font they made specifically for macOS and iOS development). As a side note, if you use this app, when you're editing the font's info, go into the Features tab as well, delete all of the features (using the minus icon in the lower left) and hit Update to let the program manage the font's features for you.
The last hurdle is clipping at the leading edge (not the top and bottom) which the ascender and descender metrics don't address. You can use the Glyphs program to edit the canvas size of individual characters to make sure they all fit but that changes the complexion of the font because it changes the character spacing too noticeably. For this problem, I think the best solution is to simply use attributed strings for your labels and text fields. And that's because attributed strings let you safely edit padding without hacking into intrinsic sizes. An example:
someLabel.attributedText = NSAttributedString(string: "Done", attributes: [NSAttributedString.Key.font: UIFont.blackItalic(size: 26), NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle.kItalicCenter])
For convenience, I extended NSMutableParagraphStyle since I use this font all over:
extension NSMutableParagraphStyle {
static var kItalicCenter: NSMutableParagraphStyle {
let s = NSMutableParagraphStyle()
s.alignment = .center
s.firstLineHeadIndent = 2
s.headIndent = 2
return s
}
}
This label will push the font forward a couple of points to prevent clipping.
I was trying to solve similar problem with diacritics in Arabic and found workaround:
I have a UITableViewCell with UILabel with arabic text, it's diacritics were cut sometimes
I overrided - (void)drawRect:(CGRect)frame to directly draw NSAttributedString on UITableViewCell
Also I decreased alpha self.arabicLabel.alpha = 0.1; to draw manually on top of label position, I still keep it to calculate cell's height
- (void)drawRect:(CGRect)frame {
[super drawRect:frame];
if (self.viewModel == nil) return;
NSAttributedString *string = [self.viewModel arabicStringWithTajweed];
CGRect originalRect = [self convertRect:self.arabicLabel.frame fromView:self.arabicLabel];
[string drawInRect:originalRect];
}
The core problem on iOS is font substitution. You are specifying a latin font, the font does not contain glyphs for the characters that will be rendered, the system uses a different font to draw the glyphs, but it is still measuring based on the original font.
Option 1, the most robust option, is to manually choose fonts that include glyphs for the characters you will render. When the font assigned to UILabel, or the attributed string it is rendering, contains all the glyphs that will be rendered, and that font has good metrics as most system fonts do, then nothing will be clipped.
Option 2, manually measure the string using glyph bounds. Make a subclass of UILabel and override textRectForBounds and possibly drawText. Measure the string with .usesDeviceMetrics. This is slower that measuring by font metrics and produces different results. For example, the strings "a" and "A" will measure differently.
Option 3, use baseline offset and line height multiple to make room for the diacritics that are being clipped. Choose or compute constant values for each font for each language, and apply those to the attributed string of the UILabel. This can compensate for the different in font metrics between the font you chose and the font that is actually rendering glyphs. We had localized strings with the worst case clipped characters for each language, and used those to compute the offset and height. Different fonts have different worst case clipping characters.

NSAttributedString: Strikethrough text with replacement text above it

I am trying to draw an NSAttributedString (actually, a constructed NSMutableAttributedString) where the "original" text has been struck and replacement text inserted above it (I'm trying to replicate the look/feel of an Ancient Greek manuscript).
My technique is a combination of NSBaselineOffsetAttributeName with NSKernAttributeName, but it appears that using a negative value for NSKernAttributeName "wipes away" the strikethrough of the text, even if the characters don't overlap.
If I put an extra space after the "A" character (in the original text), the "A" gets the strikethrough, but the "EI" is also offset to the right. So, it appears that the offset/kerning of the "EI" text affects how much of the strikethrough actually occurs.
Here's what I'd like to reproduce (I don't care about the angle; it's not about a picture-perfect reproduction; just the gist):
Here's what is currently happening:
This is when I add an extra space after the strikethrough:
So, the only other thing I can think of would be to render a separate NSAttributedString in the correct place, separate from the current one, but I have no idea how to calculate the location of a specific character in an NSAttributedString when it's drawn. I'm drawing to a PDF, not to any on-screen control like a UILabel. Alternatively, I could draw the "strikethrough" myself as a line, but that seems to still require knowing the coordinates for the text in question, which is calculated on-the-fly, and I hope to use this method to reproduce a large sample of ancient texts, which means doing it by hand just isn't a good answer here.
Anything I'm missing, or any out-of-the-box ideas to try?

Animate NSLayoutManager drawing

I have an NSLayoutManager which is drawing text using the following code:
[[self textLayoutManager] drawGlyphsForGlyphRange: NSMakeRange(0, [[self text] length])
atPoint: textFrame.origin];
in my view's -drawRect:. This works wonderfully, but what I'd really like to be able to do is animate the text in, character by character, as if it were being typed.
I've tried to append characters to a "visible string" variable, then call -[self setNeedsDisplay], but when dealing with text over approximately 20 characters, it begins to lag, as it redraws all of the text every time.
Regression: How can I animate NSLayoutManager's -drawGlyphsForGlyphRange:atPoint:?
(Disclaimer: I don't have a terrible lot of experience with the new APIs, so this is mostly coming from previous experience with text rendering.)
Your major slowdown is going to come from full-on changing the text the layout manager is working with. Even if you're just appending text, replacing the text it's using is going to cause it to throw out all its layout calculations - spacing, needed glyphs, actually reading those glyphs into RAM, applying attributes, etc. - and start over, which gets very expensive very quickly. In terms of actual NSLayoutManager, this is "invalidating the layout".
I see a couple of potential solutions off the top of my head. You could subclass NSLayoutManager ("You can create a subclass of NSLayoutManager to handle additional text attributes, whether inherent or not.") and override the showCGGlyphs:positions:count:font:matrix:attributes:inContext: to progressively ignore certain glyphs (thereby leaving the original text it's using intact). Another approach would be to emulate exactly what happens when you input text in a native container - use a mutable text storage, and append the desired text character-by-character, so that text calculation is done iteratively.
If those alone still don't have great performance, consider using those techniques in tandem with some of the native text views; although this is less true now than in past SDKs, a non-editable UITextView or a UILabel (for mutable and immutable text, respectively) contain far more optimizations than our mere mortal minds could comprehend.

Getting the character index for a x,y position in a NSString drawn with drawInRect

I'm currently using NSString's drawInRect to display a font. This is then being uploaded to a texture to be used by OpenGL.
Example:
[String drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeCharacterWrap alignment:UITextAlignmentLeft];
What I now need is a method to determine the index of the character if [x,y] were clicked.
So if I passed in [0,0] it would return index [0]. Or [24,4] would return 5 for example.
One way to do this would be if I can get the individual bounding boxes of the glyphs drawn and iterate through them.
Or is there a better way to draw the text that would allow this sort of functionality?
It is not possible to do this with the NSString and NSAttributedString drawing methods. I'd recommend one of three ways of making tappable text:
1) Core Text PRO: It's fast, it works on older versions of iOS. CON: It's got a lot of boilerplate. You'll have to create NSDictionaries of very wordy text attributes to use when creating attributed strings, then use those attributed strings inside a drawRect: implementation to create a CTFrameSetter via CTFramesetterCreateWithAttributedString(), then use the frame-setter to create a CTFrame, then use the CTFrame to draw your text. Whew! After all that, you can query the CTFrame (through even more complicated code) to find the index of a tapped character at a given CGPoint. All this is not trivial to set up.
2) TextKit PRO: It's much easier than Core Text. CON: It's only available on iOS 7. You can use UITextView, along with the methods exposed by its new properties (an NSLayoutManager and NSTextContainer), to obtain string information given a CGPoint.
3) Use TTTAttributedLabel. PRO: It's easiest of all, and works on older versions of iOS. CON: It may not provide the features you need.
Suggest you use:
NSUInteger count = [string length];
CGFloat widths[length];
for(int i=0; i<count; ++i) {
widths[i] = [string sizeWithFont:font forWidth:10000 lineBreakMode: UILineBreakModeWordWrap].width;
}
and then find the spot by walking this array

Drawing an NSAttributedString into a non-rectangular CGPath?

I generate a rather complex NSAttributedString in my iOS 3.2 application (iPad), including formatting options of type CTParagraphStyleSetting, in particular with values for kCTParagraphStyleSpecifierMinimumLineHeight and kCTParagraphStyleSpecifierParagraphSpacing.
When I try to draw this attributed string into a non-rectangular CGPath, Core Text draws it but without the line spacing defined; that is, all text appears crammed in paragraphs without line spacing. Needless to say, it does not look as pretty as if the CGPath was simply defined using a single call to CGPathAddRect()!
Is there any setting I can specify (to my CTFramesetterRef or to the CTFrameRef associated to the culprit CGPath) to avoid losing all line height information?
Thanks!
The CoreText programming guide indicates that only a rectangular CGPath is allowed:
http://developer.apple.com/library/ios/#documentation/StringsTextFonts/Conceptual/CoreText_Programming/Overview/Overview.html#//apple_ref/doc/uid/TP40005533-CH3-SW1
As of iOS 4.2, Core Text accepts non-rectangular paths. See the docs for CTFramesetterCreateFrame:
http://developer.apple.com/library/iOS/documentation/Carbon/Reference/CTFramesetterRef/Reference/reference.html#//apple_ref/c/func/CTFramesetterCreateFrame
For a sample, see "Displaying Text in a Nonrectangular Region":
https://developer.apple.com/library/iOS/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW9

Resources