Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I extract the width and height of a PNG from looking at the header in objective c?

I have a need to find the dimensions of images online without downloading them. To accomplish this I do this:

+ (CGSize) getImageDimensions:(NSString *)url {

   // Send a synchronous request
    NSMutableURLRequest * urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: url]];
    NSString *rangeString = [url hasSuffix: @"png"] ? @"bytes=0-100" : @"bytes=0-1300";
    [urlRequest setValue:rangeString forHTTPHeaderField:@"Range"];
    NSURLResponse * response = nil;
    NSError * error = nil;
    NSData * data = [NSURLConnection sendSynchronousRequest:urlRequest
                                          returningResponse:&response
                                                      error:&error];
    if (error == nil)
        return [UIImage imageWithData: data].size;
    else
        return CGSizeMake(0, 0);

}

This (downloading the first 100 bytes) surprisingly works and I get the correct dimensions for PNGs this way.

However I do not think this is a very elegant approach. First, I chose to download the first 100 bytes by just guess and checking, making it as small as possible whilst still having it work okay.

Apparently in a PNG file there's this thing called a IHDR in the header and I have to find it and directly after it are the width and height. This gives me the impression that I should loop over the data and find this IHDR and get the dimensions. The problem is, when I NSLog the data I get something like this:

... 49484452 000003b7 000001a7 08060000 006c2da0 b100000a 41694343 50494343 2050726f 66696c65 ...

I have no idea how to handle looping over my NSData object and detecting the IHDR token and then converting the things that come after it to numbers. I also have no idea if requesting just 100 bytes for a PNG is requesting too much just to get the dimensions, or if it's requesting not enough

like image 461
David Zorychta Avatar asked May 23 '13 22:05

David Zorychta


1 Answers

Rationale

According to the PNG specification:

The first eight bytes of a PNG file always contain the following (decimal) values: 137 80 78 71 13 10 26 10

So you have to read those to make sure you really have a PNG file.

Then,

The IHDR chunk must appear FIRST. It contains:

Width: 4 bytes

Height: 4 bytes

etc...

So according to the structure of chunks, you first have to read the four bytes representing the length of the chunk's data field, then the four bytes representing the chunk's name, then the two 32-bit integers representing width and height, eight bytes in total.

So, the exact minimal number of bytes you must read in order to determine width and height is 8 + 4 + 4 + 8 = 24 bytes.

Objective-C code

Once you have your NSData object, you simply have to access the bytes that it contains:

unsigned char buffer[24];
[data getBytes:buffer length:24];

Optionally, but I recommend it strongly, check that you indeed have a PNG file:

unsigned char png_header[] = {137, 80, 78, 71, 13, 10, 26, 10};
if (memcmp(buffer, png_header, 8)) {
     // this is not a PNG !
}

Make sure you have an IHDR:

unsigned char ihdr_name[] = "IHDR";
if (memcmp(buffer+8+4, ihdr_name, 4)) {
    // not an IHDR chunk, invalid PNG file
}

Width and height can be accessed as big-endian encoded unsigned ints at offset 24 minus 8 and minus 4 bytes respectively:

unsigned int width = OSReadBigInt32(buffer + 24 - 8);
unsigned int height = OSReadBigInt32(buffer + 24 - 4);

Edit: Fixed buffer reading code: PNG actually stores integers in big-endian.

like image 83
SirDarius Avatar answered Oct 24 '22 18:10

SirDarius