bad alignmentgood alignmentThis 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();
	return image;

Sample output from this code can be seen at the top-right of this post. It looks much better. Yay!


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.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.