根据图片文件获取图片的尺寸(分辨率)

背景

有这么一个需求,需要在上传图片的时候,把图片的尺寸返回给服务器。如果每次上传时都把图片数据转成UIImage,然后获得size。那么代价(cpu+内存)是比较大的。于是想着实现通过读取头几个字节来实现获得尺寸的方法。

当然不同的图片格式有不同的读取方法,本文只讲了最常见的4种图片读取:png、gif、bmp、jpg。另外还会讲一下通过第一个字节来获取类型的方式(后缀名不靠谱,有时候会骗人)。

读取相应图片格式的尺寸

png:

取出文件第16个字节到第24个字节的data。

+ (CGSize)sizeWithPngData:(NSData *)imageData
{
    if (!imageData || imageData.length < 8) {
        return CGSizeZero;
    }
    unsigned char w1 = 0, w2 = 0, w3 = 0, w4 = 0;
    [imageData getBytes:&w1 range:NSMakeRange(0, 1)];
    [imageData getBytes:&w2 range:NSMakeRange(1, 1)];
    [imageData getBytes:&w3 range:NSMakeRange(2, 1)];
    [imageData getBytes:&w4 range:NSMakeRange(3, 1)];
    int w = (w1 << 24) + (w2 << 16) + (w3 << 8) + w4;

    unsigned char h1 = 0, h2 = 0, h3 = 0, h4 = 0;
    [imageData getBytes:&h1 range:NSMakeRange(4, 1)];
    [imageData getBytes:&h2 range:NSMakeRange(5, 1)];
    [imageData getBytes:&h3 range:NSMakeRange(6, 1)];
    [imageData getBytes:&h4 range:NSMakeRange(7, 1)];
    int h = (h1 << 24) + (h2 << 16) + (h3 << 8) + h4;
    return CGSizeMake(w, h);
}

gif:

取出文件第6个字节到第10个字节的data。

+ (CGSize)sizeWithGifData:(NSData *)imageData
{
    if (!imageData || imageData.length < 4) {
        return CGSizeZero;
    }
    unsigned char w1 = 0, w2 = 0;
    [imageData getBytes:&w1 range:NSMakeRange(0, 1)];
    [imageData getBytes:&w2 range:NSMakeRange(1, 1)];
    short w = w1 + (w2 << 8);
    unsigned char h1 = 0, h2 = 0;
    [imageData getBytes:&h1 range:NSMakeRange(2, 1)];
    [imageData getBytes:&h2 range:NSMakeRange(3, 1)];
    short h = h1 + (h2 << 8);
    return CGSizeMake(w, h);
}

bmp:

取出文件第18个字节到第26个字节的data。

+ (CGSize)sizeWithBmpData:(NSData *)imageData {
    if (!imageData || imageData.length < 8) {
        return CGSizeZero;
    }
    unsigned char w1 = 0, w2 = 0, w3 = 0, w4 = 0;
    [imageData getBytes:&w1 range:NSMakeRange(0, 1)];
    [imageData getBytes:&w2 range:NSMakeRange(1, 1)];
    [imageData getBytes:&w3 range:NSMakeRange(2, 1)];
    [imageData getBytes:&w4 range:NSMakeRange(3, 1)];
    int w = w1 + (w2 << 8) + (w3 << 16) + (w4 << 24);
    unsigned char h1 = 0, h2 = 0, h3 = 0, h4 = 0;
    [imageData getBytes:&h1 range:NSMakeRange(4, 1)];
    [imageData getBytes:&h2 range:NSMakeRange(5, 1)];
    [imageData getBytes:&h3 range:NSMakeRange(6, 1)];
    [imageData getBytes:&h4 range:NSMakeRange(7, 1)];
    int h = h1 + (h2 << 8) + (h3 << 16) + (h4 << 24);
    return CGSizeMake(w, h);
}

jpg:

jpg有2种方式,第一种用于比较常见的图片:只读取前210个字节即可判断,也是最快速的。

+ (CGSize)sizeWithJpgData:(NSData *)data
{
    if ([data length] <= 0x58) {
        return CGSizeNull;
    }

    if ([data length] < 210) {// 肯定只有一个DQT字段
        short w1 = 0, w2 = 0;
        [data getBytes:&w1 range:NSMakeRange(0x60, 0x1)];
        [data getBytes:&w2 range:NSMakeRange(0x61, 0x1)];
        short w = (w1 << 8) + w2;
        short h1 = 0, h2 = 0;

        [data getBytes:&h1 range:NSMakeRange(0x5e, 0x1)];
        [data getBytes:&h2 range:NSMakeRange(0x5f, 0x1)];
        short h = (h1 << 8) + h2;
        return CGSizeMake(w, h);
    } else {
        short word = 0x0;
        [data getBytes:&word range:NSMakeRange(0x15, 0x1)];
        if (word == 0xdb) {
            [data getBytes:&word range:NSMakeRange(0x5a, 0x1)];
            if (word == 0xdb) {// 两个DQT字段
                short w1 = 0, w2 = 0;
                [data getBytes:&w1 range:NSMakeRange(0xa5, 0x1)];
                [data getBytes:&w2 range:NSMakeRange(0xa6, 0x1)];
                short w = (w1 << 8) + w2;

                short h1 = 0, h2 = 0;
                [data getBytes:&h1 range:NSMakeRange(0xa3, 0x1)];
                [data getBytes:&h2 range:NSMakeRange(0xa4, 0x1)];
                short h = (h1 << 8) + h2;
                return CGSizeMake(w, h);
            } else {// 一个DQT字段
                short w1 = 0, w2 = 0;
                [data getBytes:&w1 range:NSMakeRange(0x60, 0x1)];
                [data getBytes:&w2 range:NSMakeRange(0x61, 0x1)];
                short w = (w1 << 8) + w2;
                short h1 = 0, h2 = 0;

                [data getBytes:&h1 range:NSMakeRange(0x5e, 0x1)];
                [data getBytes:&h2 range:NSMakeRange(0x5f, 0x1)];
                short h = (h1 << 8) + h2;
                return CGSizeMake(w, h);
            }
        } else {
            return CGSizeNull;
        }
    }
}

第二种方式,遍历data,所以使用filePath一点点读

+ (CGSize)sizeWithJpgFilePath:(NSString *)filePath
{
    if (!filePath.length) {
        return CGSizeNull;
    }
    if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        return  CGSizeNull;
    }

    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
    NSUInteger offset = 2;
    NSUInteger length = 0;
    while (1) {
        [fileHandle seekToFileOffset:offset];
        length = 4;
        NSData *data = [fileHandle readDataOfLength:length];
        if (data.length != length) {
            break;
        }
        offset += length;
        int marker,code;
        NSUInteger newLength;
        unsigned char value1,value2,value3,value4;
        [data getBytes:&value1 range:NSMakeRange(0, 1)];
        [data getBytes:&value2 range:NSMakeRange(1, 1)];
        [data getBytes:&value3 range:NSMakeRange(2, 1)];
        [data getBytes:&value4 range:NSMakeRange(3, 1)];
        marker = value1;
        code = value2;
        newLength = (value3 << 8) + value4;
        if (marker != 0xff) {
            [fileHandle closeFile];
            return CGSizeNull;
        }

        if (code >= 0xc0 && code <= 0xc3) {
            length = 5;
            [fileHandle seekToFileOffset:offset];
            NSData *data =[fileHandle readDataOfLength:length];
            if (data.length != length) {
                break;
            }
            Byte *bytesArray = (Byte*)[data bytes];
            NSUInteger height = ((unsigned char)bytesArray[1] << 8) + (unsigned char)bytesArray[2];
            NSUInteger width =  ((unsigned char)bytesArray[3] << 8) + (unsigned char)bytesArray[4];
            [fileHandle closeFile];
            return CGSizeMake(width, height);
        }
        else {
            offset += newLength;
            offset -=2;
        }
    }
    [fileHandle closeFile];
    return CGSizeNull;

}             

第一种:像新浪微博和一些著名的网站的jpg图片,用第一种基本能找出来。而且速度很快。
第二种:可以找出一些百度上随意下载过来的图片。虽然比第一种耗费CPU,但比起开辟一个UIImage,还是有优势的。 建议先用第一种查,查不出来再使用第二种方式。

读取图片的格式

取出文件第1个字节的data。

+ (NSString *)imageTypeWithimageData:(NSData *)imageData
{
    //判断数据是否存在
    if (!imageData) {
        return nil;
    }

    //第一个字节进行判断
    uint8_t c;
    [imageData getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x42:
            return @"image/bmp";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52:
            // R as RIFF for WEBP
            if ([imageData length] < 12) {
                return nil;
            }

            NSString *testString = [[NSString alloc] initWithData:[imageData subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }

            return nil;
    }
    return nil;
}

总结

可以看出基本上只需要读取图片数据头210个字节,基本就能确定一张图片的类型和尺寸。而且这个原理也可以应用到下载网络图片。

参考

http://cxjwin.github.io/2013/11/05/previewimagesize/

demo

https://github.com/LiZunYuan/imageTypeAndSizeDemo