iPhone Images from Character Glyphs

WTFBar.png

In which a category allowing the creation of UIImages from Unicode characters, suitable for use as Tab Bar icons, is created, but a state of mild displeasure at the implementation of said category is engendered.

[If you just want code, with none o’ that darn readdin’, there’s a zip at the end of the post]

Adding a ‘test’ tab to my in-development iPhone app, I had a dilemma. My troublesome aesthetic sense was telling me that, despite being seen by no-one but me, it needed a good looking icon. My sense of efficiency, though, was telling me “No! Don’t spend the time to create an icon for something no-one will ever see!”.

In the back of my brain a solution formed though. I’ve been working with Unicode recently, and there are a lot of characters you night not expect there, from the more common, like arrows (‘➡’), card suits (‘♣’), and unusual punctuation (‘‽’ interrobang - I love that one), to the more esoteric things, like scissors (‘✂’), aeroplanes (‘✈’), and skulls (‘☠’). On a Mac, you can pull-up the “Edit”→“Special Characters…” dialog and browse through them. The simplicity of many of these glyphs mean that they look well-matched to the iPhone’s interface style. Why not just use one of these ready-made images as-is? The check-mark glyph (‘✓’), specifically, seemed well-suited to my new ‘Test’ tab.

A UIImage category with the ability to create an image from a Unicode character seemed like the solution. I could just ask it for a 29x29 image of a check-mark, and I’d be set! It didn’t seem like it would be hard to write either - just render the string into an image-backed context.

Turns out it wasn’t that easy though, so as well as presenting my solution, this is a bit of a Pimp My Code post - if you can do this better, I’d like to see it, because my way seems unfortunately non-optimal (you’ll see why later).

Anyway, here’s the code:

+ (UIImage *)imageWithString:(NSString *)string // What we want an image of.
                        font:(UIFont *)font     // The font we'd like it in.
                        size:(CGSize)size       // Size of the desired image.
{
    // Create a context to render into.
    UIGraphicsBeginImageContext(size);

    // Work out what size of font will give us a rendering of the string
    // that will fit in an image of the desired size.

    // We do this by measuring the string at the given font size and working
    // out the ratio scale to it by to get the desired size of image.

    // Measure the string size.
    CGSize stringSize = [string sizeWithFont:font];

    // Work out what it should be scaled by to get the desired size.
    CGFloat xRatio = size.width / stringSize.width;
    CGFloat yRatio = size.height / stringSize.height;
    CGFloat ratio = MIN(xRatio, yRatio);

    // Work out the point size that'll give us the desired image size, and
    // create a UIFont that size.
    CGFloat oldFontSize = font.pointSize;
    CGFloat newFontSize = floor(oldFontSize * ratio);
    ratio = newFontSize / oldFontSize;
    font = [font fontWithSize:newFontSize];

    // What size is the string with this new font?
    stringSize = [string sizeWithFont:font];

    // Work out where the origin of the drawn string should be to get it in
    // the centre of the image.
    CGPoint textOrigin = CGPointMake((size.width - stringSize.width) / 2,
                                     (size.height - stringSize.height) / 2);

    // Draw the string into out image!
    [string drawAtPoint:textOrigin withFont:font];

    // We're done!  Grab the image and return it!
    // (Don't forget to end the image context first though!)
    UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return retImage;
}

Looking good. Now, all we have to do is use it to render a check-mark as the UIViewController subclass tab bar icon, in its init method:

self.tabBarItem.image =
    [UIImage imageWithString:@"\u2713" // Unicode code for a check-mark.
                        font:[UIFont systemFontOfSize:
                              [UIFont systemFontSize]]
                        size:CGSizeMake(29, 29)];

Easy! So how does it look?

WTFBar.png

Hrm. It sure is nice and shiny, and it certainly looks like the image I wanted, but it’s, well, rather small. What about all that size manipulation we did? Why’s it not filling the full 29X29 pixels?

Well, we forgot that a given string doesn’t necessarily (in fact, I’d go so far as to say never does) ‘fill’ its entire bounding box. This is more obvious for letters - for example, ask how large a rendering if the string @”x” is and you’ll get a box that is basically as tall as the line height, not just the lowercase ‘x’ - but it’s true for symbols too. What we actually needed to scale was the size of the glyph’s bounding box, not the size of the rendered string.

This is where I hit a wall. The iPhone does have ways to get the bounding boxes of (and to draw) raw glyphs, but these APIs work at the font level, where glyphs have potentially font-unique arbitrary numbers assigned to them - numbers that don’t corresponding to their ‘equivalent’ Unicode characters - and there’s unfortunately no public way to convert from an arbitrary character to its glyph number. This means that there’s no way to get a bounding box for a character or string - at least, not that I could find.

What to do? I didn’t want to be defeated, so it was down to good old-fashioned bitmap manipulation. Here’s the routine again, with some extra scaling:

+ (UIImage *)imageWithString:(NSString *)string // What we want an image of.
                        font:(UIFont *)font     // The font we'd like it in.
                        size:(CGSize)size       // Size of the desired image.
{
    // Create a context to render into.
    UIGraphicsBeginImageContext(size);

    // Work out what size of font will give us a rendering of the string
    // that will fit in an image of the desired size.

    // We do this by measuring the string at the given font size and working
    // out the ratio scale to it by to get the desired size of image.

    // Measure the string size.
    CGSize stringSize = [string sizeWithFont:font];

    // Work out what it should be scaled by to get the desired size.
    CGFloat xRatio = size.width / stringSize.width;
    CGFloat yRatio = size.height / stringSize.height;
    CGFloat ratio = MIN(xRatio, yRatio);

    // Work out the point size that'll give us the desired image size, and
    // create a UIFont that size.
    CGFloat oldFontSize = font.pointSize;
    CGFloat newFontSize = floor(oldFontSize * ratio);
    ratio = newFontSize / oldFontSize;
    font = [font fontWithSize:newFontSize];

    // What size is the string with this newfont?

    stringSize = [string sizeWithFont:font]



   // Work out where the origin of the drawn string should be to get it in

   // the centre of the image.
    CGPoint textOrigin = CGPointMake((size.width - stringSize.width) / 2,
                                     (size.height - stringSize.height) / 2);

    // Draw the string into out image.
    [string drawAtPoint:textOrigin withFont:font];

    // We actually don't have the scaling right, because the rendered
    // string probably doesn't actually fill the entire pixel area of the
    // box we were given.  We'll use what we just drew to work out the /real/
    // size we need to draw at to fill the image.

    // First, we work out what area the drawn string /actually/ covered.

    // Get a raw bitmap of what we've drawn.
    CGImageRef maskImage = [UIGraphicsGetImageFromCurrentImageContext()
                                CGImage];
    CFDataRef imageData = CGDataProviderCopyData(
                              CGImageGetDataProvider(maskImage));
    uint8_t *bitmap = (uint8_t *)CFDataGetBytePtr(imageData);
    size_t rowBytes = CGImageGetBytesPerRow(maskImage);

    // Now, go through the pixels one-by-one working out the area in which the
    // image is not still blank.
    size_t minx = size.width, maxx = 0, miny = size.height, maxy = 0;
    uint8_t *rowBase = bitmap;
    for(size_t y = 0; y < size.width; ++y, rowBase += rowBytes) {
        uint8_t *component = rowBase;
        for(size_t x = 0; x < size.width; ++x, component += 4) {   
            if(*component != 0) {
                if(x < minx) {
                    minx = x;
                } else if(x > maxx) {
                    maxx = x;
                }
                if(y < miny) {
                    miny = y;
                } else if(y > maxy) {
                    maxy = y;
                }
            }
        }
    }
    CFRelease(imageData); // We're done with this data now.

    // Put the area we just found into a CGRect.
    CGRect boundingBox =
        CGRectMake(minx, miny, maxx - minx + 1, maxy - miny + 1);

    // We're going to have to move string we're drawing as well as scale it,
    // so we work out how the origin we used to draw the string relates to the
    // 'real' origin of the filled area.
    CGPoint goodBoundingBoxOrigin =
        CGPointMake((size.width - boundingBox.size.width) / 2,
                    (size.height - boundingBox.size.height) / 2);
    CGFloat textOriginXDiff = goodBoundingBoxOrigin.x - boundingBox.origin.x;
    CGFloat textOriginYDiff = goodBoundingBoxOrigin.y - boundingBox.origin.y;

    // Work out how much we'll need to scale by to fill the entire image.
    xRatio = size.width / boundingBox.size.width;
    yRatio = size.height / boundingBox.size.height;
    ratio = MIN(xRatio, yRatio);

    // Now, work out the font size we really need based on our scaling ratio.
    // newFontSize is still holding the size we used to draw with.
    oldFontSize = newFontSize;
    newFontSize = floor(oldFontSize * ratio);
    ratio = newFontSize / oldFontSize;
    font = [font fontWithSize:newFontSize];

    // Work out where to place the string.
    // We offset the origin by the difference between the string-drawing origin
    // and the 'real' image origin we measured above, scaled up to the new size.
    stringSize = [string sizeWithFont:font];
    textOrigin = CGPointMake((size.width - stringSize.width) / 2,
                             (size.height - stringSize.height) / 2);   
    textOrigin.x += textOriginXDiff * ratio;
    textOrigin.y += textOriginYDiff * ratio;

    // Clear the context to remove our old, too-small, rendering.
    CGContextClearRect(UIGraphicsGetCurrentContext(),
                       CGRectMake(0, 0, size.width, size.height));

    // Draw the string again, in the right place, at the right size this time!
    [string drawAtPoint:textOrigin withFont:font];

    // We're done!  Grab the image and return it!
    // (Don't forget to end the image context first though!)
    UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return retImage;
}

Phew! That was quite a palaver! Things would certainly be much easier if we could just ask for a string’s literal bounding box. The proof of the pudding is in the eating though, as they say. Does it work? Here’s what the check-mark looks like now:

WTFBar.png

Pretty good, if I do say so myself (and you can look again at the image at the top of the post for more examples of it working).

So; a day spent, just to save time, creating code to create an icon I could have drawn by hand in five minutes, and then more time writing a blog post about it. Still, it’s reusable - and that’s what counts! Right?…

Here’s the source in a proper format, to save your copy-and-paste fingers. Let me know if you use it, it would certainly be nice if it found some use somewhere besides my test tab. Do also let me know if you find a better way to do similar - my sense of efficiency is still reeling at all the bitmap manipulation.

downloadZip.png

[1] Check out the “Apple Symbols” font, specifically in the “Glyph” view - there’s lots of useful GUI ‘artwork’ in there. Near the end, there’s even a lot of iPhone-related glyphs. Unfortunately the font is not included on the iPhone


12 Comments

Also, Unicode has a snowman: http://unicodesnowmanforyou.com/


Unfortunately, the only one available on the iPhone is not nearly as cool looking as the default one on OS X.


Great article. I learned a lot.

Now, is there a way to get that image and create a file so that we could just reference the file and not render each time? ideas?


@Anonymous Thanks! Saving the image to a file is pretty easy. For example, to get the check-mark ‘tick’ image into a file:

UIImage *tickImage = [UIImage imageWithString:@"\u2713" font:[UIFont systemFontOfSize:[UIFont systemFontSize]] size:CGSizeMake(29, 29)];

NSData *tickPngRepresentation = UIImagePNGRepresentation(tickImage);

[tickPngRepresentation writeToFile:<path to file> atomically:NO];

where <path to file> is an NSString containing the path to where you want to file to be written.

Depending on where you’re using it though, it might not be worth the effort (generating it on the fly could well be fast enough).


Hi, brilliant work. BUT can you change text color?? this would be very useful if u want to use the image in cell.image

thanks and let me know at [REDACTED]


@Alamin Of course you can change the text colour - a simple call to whatever UIColor you want to use (like "[[UIColor redColor] set];") before the final drawAtPoint: will take care of that.

Note that changing the colour the string’s drawn in won’t change what a UITabBarItem does with the image - it relies solely on the alpha channel and renders that shiny blueness of its own accord. It would though work for other uses of the image, like in a UITableViewCell, as you suggest.


Of course, you could always open up Font Book and Grab the icons you want…


You’re a fellow obsessive like me! I like that. :-)

Did you ever put any more time into this? You should be able to get the exact width of the bounding box (plus advance) if you use the CGContext … text functions. You would use:

o CGContextSetTextPosition( context, orgX, orgY ) to set an initial origin, call o CGContextShowText( context, ℑ o CGPoint next = CGContextGetTextPosition( context ) to see which x the pen has advanced to.

Worked for me at any rate.

I wonder if you’ll ever see this email, given what I’m sure is the overwhelming avalanche of emails currently hitting your server. Congrats on that fabulous outcome btw; I’ve just purchased your app in support. Illegitimis non carborundum! :-)

Howard


@Howard I think that that still has the same problem - that the bounding box includes the advance. I want the bounding box of only the actual painted areas of the glyph.


In the routine imageWithString with extra scaling (the second routine given), the line:

for(size_t y = 0; y size.width; ++y, rowBase += rowBytes) {

should read:

for(size_t y = 0; y size.height; ++y, rowBase += rowBytes) {

That is, width is replaced by height.

Thanks for the code.


Hello Jamie:

I tried your code in iOS 4 SDK, found out that I have to change uint8_t to uint32_t, or blank pixel finding algorithm will not work. Then maxx will be smaller than minx, boundingBox.size.width will become some crazy big value. In the end I will get "CGAffinetransformInvert: singular matrix" error.

After I change uint8_t to uint32_t, it still has some layout offset problem, try to fix it tomorrow.


ชิเนเต้ เบบี้เฟซ

If input is cgimageref is this code still work?

thanks