This one is a little personal. I have an idiosyncratic, and probably ill-advised, interest in getting ASCII art to display properly on the iPhone. (This was actually one of the first things I experimented with on the platform.) My efforts have been hampered by the fact that the iPhone’s “fixed-width” fonts aren’t; whitespace is assigned a different width from everything else. Today, I discuss a small function that uses Quartz 2D to draw proper fixed width fonts.
The Problem
In a nutshell, Cocoa’s text subsystem doesn’t render all the characters in a fixed-width font with the same width. Consider this function, clipped from an illustrative subclass of UITextView
. It takes an NSString
, representing a 53×24 text buffer, inserts newlines, and sets the text
property of its instance to the result.
// Member function of a UITextView subclass
- (void)setTextBuffer:(NSString*)s
{
// 53-character screen pitch is a judgement call
NSUInteger tw=53;
// These could just be set once; they're set here for illustration
self.font = [UIFont fontWithName:@"CourierNewPS-BoldMT" size:9];
self.backgroundColor = [UIColor blackColor];
self.textColor = [UIColor greenColor];
// Cut up a 1272-character string into 24 rows of 53 characters, split by newlines
NSMutableArray* a = [[NSMutableArray alloc] init];
for (NSUInteger c = 0, n = tw, l = [s length]; c < l; c = n, n += tw)
{
[a addObject:[s substringWithRange:NSMakeRange(c, (n<l?n:l)-c)]];
}
self.text = [a componentsJoinedByString:@"\n"];
[a release];
}
Sample output from this code can be seen at the top-left of this post. As you can see, the results are unacceptable. (That’s supposed to be an ASCII art input box, in case it wasn’t obvious.)
The Solution
Happily, Quartz 2D’s text subsystem does render fixed-width fonts properly. (Which raises some interesting question about what Cocoa is doing, and exactly how many text subsystems are in iPhone OS, anyway.) Here’s some code that uses Quartz 2D to draw an image of fixed-width text:
UIImage* renderScreen(NSString* s)
{
NSUInteger ch=10, tw=53, th=24;
// Clip
if ([s length] > tw*th) s = [s substringToIndex:tw*th];
// Create and get a pointer to context
UIGraphicsBeginImageContext(CGSizeMake(320, ch*th));
CGContextRef context = UIGraphicsGetCurrentContext();
// Convert co-ordinate system to Cocoa’s (origin in UL, not LL)
CGContextTranslateCTM(context, 0, ch*th);
CGContextConcatCTM(context, CGAffineTransformMakeScale(1, -1));
// Clear background
CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextFillRect(context, CGRectMake(0, 0, 320, ch*th));
// Set fill and stroke colors
CGContextSetFillColorWithColor(context, [[UIColor greenColor] CGColor]);
CGContextSetStrokeColorWithColor(context, [[UIColor greenColor] CGColor]);
// Set text parameters
CGContextSelectFont(context, "CourierNewPS-BoldMT", 10, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);
// Draw text
NSString* ss;
for (NSUInteger c = 0, n = tw, l = [s length], row = (th-1)*ch; c < l; c = n, n += tw, row -= ch)
{
ss = [s substringWithRange:NSMakeRange(c, (n<l?n:l)-c)];
CGContextShowTextAtPoint(context, 1, row, [ss cStringUsingEncoding:NSMacOSRomanStringEncoding], [ss lengthOfBytesUsingEncoding:NSMacOSRomanStringEncoding]);
}
// Make and return image
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Sample output from this code can be seen at the top-right of this post. It looks much better. Yay!
Pitch
A quick note about the unusual pitch (53 characters) used in this code: This value was picked because it almost divides evenly into 320. (53 characters x 6 pixels per character = 318 pixels.) The iPhone screen is 320 pixels across, 6 pixels is about the minimum legible width, and I wanted to get as many characters on screen as possible.
Pingback: Things that were not immediately obvious to me » Blog Archive » iCurses