Sunday 3 March 2013

Creating UIImages from TGA Data

UIImage doesn't support as wide a range of file types as NSImage does in Cocoa . It's understandable, given the different natures of the devices they run on, however there are times when you will want to load data in a format that's not directly supported by UIImage. This happened recently while experimenting with creating a Milkshape 3D model loader for use in iPhone game development. Milkshape uses Targa and PCX files for the texture, and neither of these formats is supported by UIImage.

I haven't tackled PCX files, but I wrote a category on UIImage to let you instantiate a UIImage object from Targa data stored in a TGA file. This version doesn't support RLE encoded files, but generally for game models, RLE isn't encoded. If you need to save size in your image executable, zipping the uncompressed TGA data will save about the same amount of room. If you're looking at doing a game, I probably wouldn't recommend compressing the images at all unless you're very close to that ten meg limit that prevents people from downloading your game over 3G & Edge, since you'll have to expend processing cycles to decompress at launch or load time.

You can find the category and an Xcode project that shows how it can be used in the iPhone Bits repository over at Google Code.

This is a first draft and there is likely room for improvement. There are some known inefficiencies in this class. For example, the iPhone's native byte ordering is BGRA, and so is Targa's, This code, however, converts the Targa data to RGB, and likely under the hood, UIImage is converting back to BGRA. I'm pretty sure Core Graphics provides a way to avoid this conversion, but don't have time to research it right now. Also, if your only use of the Targa image is going to be in OpenGL, you probably want to skip this category altogether and just load the bitmap data into OpenGL directly. This category combined with the Texture2D from Apple's sample code certainly gives an easy way to setup a scaffold environment for early development.

UIImage-Targa.h
#import <UIKit/UIKit.h>

@interface UIImage(Targa)
+(id)imageFromTGAFile:(NSString *)filename;
+(id)imageFromTGAData:(NSData *)data;
+(id)imageWithRawRGBAData:(NSData *)data width:(int)width height:(int)height;@end


UIImage-Targa.m
#import "UIImage-Targa.h"

void releaseImageData(void *info, const void *data, size_t size) 
{
    free((void *)data);
}

@implementation UIImage(Targa)
+(id)imageFromTGAFile:(NSString *)filename{
    NSData *data = [[NSData alloc] initWithContentsOfFile:filename];
    id ret = [self imageFromTGAData:data];
    [data release];
    return ret;
}
+(id)imageFromTGAData:(NSData *)data{
    
                short       imageHeight;
                short       imageWidth;
    unsigned    char        bitDepth;
    unsigned    char        imageType;
                int         colorMode;
                long        imageSize;
    unsigned    char        *imageData;
    
    unsigned    char *bytes = (unsigned char *)[data bytes]+2; // skip first two bytes    
    memcpy(&imageType, bytes,  sizeof(unsigned char));
    
    if (imageType != 2 && imageType != 3)
    {
        NSException *exception = [NSException exceptionWithName:@"Unsupported File Format" 
                                                         reason:@"Compressed TGA files are not supported yet"
                                                       userInfo:nil];
        [exception raise];
    }
    bytes += ( (sizeof(unsigned char) * 2) + (sizeof(short) * 4));
    memcpy(&imageWidth, bytes, sizeof(short));
    bytes += 2;
    memcpy(&imageHeight, bytes, sizeof(short));
    bytes += 2;
    memcpy(&bitDepth, bytes, sizeof(char));
    bytes+=2;
    
    colorMode = bitDepth / 8;
    imageSize = imageWidth * imageHeight * colorMode; 

    long newDataLength = imageWidth * imageHeight * 4;
    imageData = (unsigned char *)malloc(newDataLength);
    memcpy(imageData, bytes, imageSize * sizeof(unsigned char));
    
    // Targa is BGR, swap to RGB    long byteCounter = 0;
    for (long i = 0; i < imageSize; i += colorMode) 
    { 
        long start = (byteCounter++ * 4);
        imageData[start] = bytes[i+2];
        imageData[start+1] = bytes[i+1];
        imageData[start+2] = bytes[i];
        imageData[start+3] = (colorMode == 4) ? bytes[i+3] : 1.0;
    } 
    
    // Targa uses more standard Y axis, so need to swap top & bottom bytes    // We can swap 32 bits at a time rather than going byte by byte    uint32_t *imageDataAsInts = (uint32_t *)imageData;
    for (int y = 0; y < imageHeight / 2; y++)
    {
        for (int x = 0; x < imageWidth; x++)
        {
            uint32_t top = imageDataAsInts[y * imageWidth + x];
            uint32_t bottom = imageDataAsInts[(imageHeight - 1 - y) * imageWidth + x];
            imageDataAsInts[(imageHeight - 1 - y) * imageWidth + x] = top;
            imageDataAsInts[y * imageWidth + x] = bottom;
        }
    }

    NSData *swappedData = [[NSData alloc] initWithBytes:imageData length:newDataLength];
    return [self imageWithRawRGBAData:swappedData width:imageWidth height:imageHeight];
}
+(id)imageWithRawRGBAData:(NSData *)data width:(int)width height:(int)height{
    const void * buffer = [data bytes];
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, [data length], releaseImageData);

    const int bitsPerComponent = 8;
    const int bitsPerPixel = 4 * bitsPerComponent;
    const int bytesPerRow = 4 * width;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;

    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    CGDataProviderRelease(provider);
    
    UIImage *myImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    
    return myImage;
}@end

3 comments:

  1. Awesome work, so if you have any tga, avi, jpg file and want to convert it on another file then use this converter;

    File Spinner

    ReplyDelete