Amy Worall asks:
How do I centre an image in DTCoreText? I’m working with attributed strings not HTML. Any sample code?
In this blog post will answer this.
DTCoreText consists of multiple parts: tools for converting between HTML and attributed strings, a custom layout engine doing the “frame setting” and (on iOS) several UI classes for displaying those attributed strings.
For most scenarios you would want to simply pass in some HTML data and display that, but Amy’s use case seems to forego this step. And there is nothing wrong with skipping directly to putting together an NSAttributedString. As long as you use the same attributes that the HTML parser would put in there the result should be identical.
Amy is trying to get a divider image centered amongst the text. This should be a simple exercise, but somehow it comes out as being off center. Looking at the sample code she provided didn’t yield any clues as to what could be reason for this.
My first thought when seeing this was that this has to be a bug in DTCoreText. Feelings of Panic starting to rise.
Try it in the Demo App
So my search for the cause of the off-centering began with the DTCoreText demo app. I like to make sure I have the latest version of DTCoreText (and submodules) cloned and then I try to find a place where I can test the text.
If I have HTML that does not seem to come out right, then I paste it into the CurrentTest.html which is the last menu option in the demo’s menu of snippets.
So the first test I am doing is to see if maybe centered paragraphs are broken somehow. In HTML you can switch to centered paragraph styles with the <center> tag or with the equivalent CSS text-align attribute. Images that are contained in the app bundle can be referenced without any path.
<html> <head> <body> <center><img src="Oliver.jpg" width="100" height="100"></center> <p> Lorem ipsum dolor sit amet, ... </p> </body> </html>
Push the “Debug Frames” button to have DTCoreText draw additional backgrounds behind the glyph runs, the frame outline. There is also a dashed centering line.
We can see that the image is centered on the centering line. At least we know that the problem must lie elsewhere.
Inspecting the Chars view shows that the image paragraph consists of the Unicode Object Placeholder @”\ufffc” character and a newline (code 10). The Ranges view shows that the placholder character has a DTTextAttachment attribute, a run delegate attribute as well as a paragraph style with alignment 2 for centering.
The placeholder character is not being displayed itself. Instead CoreText calls the run delegate to get the dimensions from the text attachment. The run delegate asks the DTTextAttachment for Ascent, Descent and Width to be reserved so that the image has enough space to fit. Since the run delegate is a set of C-functions it has no context from being in the scope of an object. For this purpose there is a context parameter in which we pass a pointer to the text attachment.
The text attachment object and the run delegate always exist retained by the same attribute range this is possible, even under ARC.
And Now in Code
Amy provided the following gist of the code she’s been experimenting with. With what I explained above, can you spot the problem?
NSMutableParagraphStyle *centredPS = [[NSMutableParagraphStyle alloc] init]; centredPS.alignment = NSTextAlignmentCenter; NSMutableAttributedString *attributedStringBuilder = [NSMutableAttributedString new]; DTTextAttachment *attachment = [[DTTextAttachment alloc] init]; UIImage *image = [UIImage imageNamed:@"divider"]; attachment.contents = image; attachment.contentType = DTTextAttachmentTypeImage; attachment.displaySize = image.size; NSMutableDictionary *newAttributes = [NSMutableDictionary new]; [newAttributes setObject:attachment forKey:NSAttachmentAttributeName]; [newAttributes setObject:centredPS forKey:NSParagraphStyleAttributeName]; [attributedStringBuilder appendAttributedString:[[NSAttributedString alloc] initWithString:UNICODE_OBJECT_PLACEHOLDER attributes:newAttributes]]; [attributedStringBuilder appendString:@"\n"]; // then append all our normal paragraphs of text. // This is eventually given to a DTAttributedTextView to display. |
And no, I’m not referring to Amy spelling “center” incorrectly throughout. 🙂
The run delegate is missing.
If I paste this code into DemoAboutViewController, reducing the attachment display size to 100×100. This comes out quite weird.
Apparently this spaces the lines correctly from the top (done in DTCoreTextLayoutFrame), but the ascender of the image (the part going up from the base line) is only as high as the font’s ascender. This explains why in Amy’s sample image the divider looks like it is vertically in the correct position. It has a height similar to the font height.
Let’s add in the missing run delegate.
// need run delegate for sizing CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate((id)attachment); [newAttributes setObject:(__bridge id)embeddedObjectRunDelegate forKey:(id)kCTRunDelegateAttributeName]; CFRelease(embeddedObjectRunDelegate); |
Build&Run and we can marvel at the correct placement of my face.
Pretty, eh? (I mean the positioning)
To simplify things I have this category on NSAttributedString in DTRichTextEditor for inserting images into the editable text.
+ (NSAttributedString *)attributedStringWithImage:(UIImage *)image maxDisplaySize:(CGSize)maxDisplaySize { DTTextAttachment *attachment = [[DTTextAttachment alloc] init]; attachment.contents = (id)image; attachment.originalSize = image.size; attachment.contentType = DTTextAttachmentTypeImage; CGSize displaySize = image.size; if (!CGSizeEqualToSize(maxDisplaySize, CGSizeZero)) { if (maxDisplaySize.width < image.size.width || maxDisplaySize.height < image.size.height) { displaySize = sizeThatFitsKeepingAspectRatio(image.size,maxDisplaySize); } } attachment.displaySize = displaySize; DTHTMLElement *element = [[DTHTMLElement alloc] init]; element.textAttachment = attachment; return [element attributedString]; } |
This constructs the attributed string with the image exactly like DTCoreText does, via DTHTMLElement. There you can find that the run delegate is being added if the attachment property is not nil. This also adjusts the display size to fit a given maximum display size unless you pass CGSizeZero for that.
Conclusion
In DTCoreText (on iOS) besides a DTTextAttachment we need to also specify a run delegate so that Core Text is able to reserve the appropriate space for it. On Mac attributed strings can have attachments as well. There they are embedded in NSTextAttachment instances which have a file wrapper for the file as well as a cell for sizing and displaying the content.
Apple has brought over NSTextAttachment in iOS 6, but kept it private API. This is why I came up with this workaround using the killer combo of Run Delegate + DTTextAttachment. This is compatible all the way down to iOS 4.
Categories: Q&A