ImageIO

0x00 前言

这篇文章是自己在学习 CFHipsterRef Chapter 8 ImageIO 时做的笔记。

0x01 ImageIO

Image I/O 是一个强大的,虽然不太知名的图像处理框架。独立于 Core Graphics,它可以在许多不同格式之间读写,访问照片元数据,并执行常见的图像处理操作。这个框架提供了平台上最快的图像编码器和解码器,具有高级缓存机制,甚至能够增量地加载图像。

0x02 Supported Image Types

根据官网文档,Image I/O 支持 “大多数图像格式”。与其拿文档来看,还不如用代码的方式来获取。

CGImageSourceCopyTypeIdentifiers 返回支持的图像类型的 UTI 列表:

Image I/O UTIs

UTI iOS OS X
com.adobe.photoshop-image x
com.adobe.raw-image x x
com.apple.icns x
com.apple.macpaint x
com.apple.pict x
com.apple.quicktime-image x
com.canon.cr2-raw-image x x
com.canon.crw-raw-image x x
com.canon.tif-raw-image x x
com.compuserve.gif x x
com.epson.raw-image x x
com.fuji.raw-image x x
com.hasselblad.3fr-raw-image x x
com.hasselblad.fff-raw-image x x
com.ilm.openexr-image x
com.kodak.flashpix-image x
com.kodak.raw-image x x
com.konicaminolta.raw-image x x
com.leafamerica.raw-image x x
com.leica.raw-image x x
com.leica.rwl-raw-image x x
com.microsoft.bmp x x
com.microsoft.cur x x
com.microsoft.ico x x
com.nikon.nrw-raw-image x x
com.nikon.raw-image x x
com.olympus.or-raw-image x x
com.olympus.raw-image x x
com.olympus.sr-raw-image x x
com.panasonic.raw-image x x
com.panasonic.rw2-raw-image x x
com.pentax.raw-image x x
com.samsung.raw-image x x
com.sgi.sgi-image x
com.sony.arw-raw-image x x
com.sony.raw-image x x
com.sony.sr2-raw-image x x
com.truevision.tga-image x x
public.jpeg x x
public.jpeg-2000 x x
public.mpo-image x
public.png x x
public.radiance x
public.tiff x x
public.xbitmap-image x x

事实证明,这似乎是 大多数 格式。至少是那些对现在应用程序重要的。普遍支持常见格式:TIFF,JPEG,GIF,PNG,RAW 和 Windows Bitmap,Icon 和 Cursor。另外,在 iOS 上支持多种供应商特定的 RAW 摄像机格式,不过 OS X 还支持其他几种格式。

0x03 Writing to a file

Image I/O 提供了高级输出配置,不需要很多开销。

指定期望输出格式的 UTI 以及任何选项,如压缩质量,方向或是否忽略 Alpha 通道。为目标创建一个 CGImageDestinationRef,将 CGImage 添加到其中,然后进行最终化(finalized):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImage *image = ...;

NSURL *fileURL = [NSURL fileURLWithPath:@"/path/to/output.jpg"];
NSString *UTI = @"public.jpeg";
NSDictionary *options = @{
(__bridge id)kCGImageDestinationLossyCompressionQuality : @(0.75),
(__bridge id)kCGImagePropertyOrientation : @(4),
(__bridge id)kCGImagePropertyHasAlpha : @(NO)
};

CGImageDestinationRef imageDestinationRef = CGImageDestinationCreateWithURL((__bridge CFURLRef)fileURL, (__bridge CFStringRef)UTI, 1, NULL);

CGImageDestinationAddImage(imageDestinaRef, [image CGImage], (__bridge CFDictionaryRef)options);
CGImageDestinationFinalize(imageDestinationRef);
CFRelease(imageDestinationRef);

0x04 Reading from a File

从一个文件中读取一张图像的过程也非常相似。

创建一个期望的输出文件的 file URL,并设置任何所需的缓存标记或图像类型提示。使用这个 URL 创建一个 CGImageSourceRef,然后调用 CGImageSourceCreateImageAtIndex 读取数据并创建一个 CGImage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSURL *fileURL = [NSURL fileURLWithPath:@"/path/to/input.jpg"];
NSDictionary *options = @{
(__bridge id)kCGImageSourceTypeIdentifierHint : @"public.jpeg",
(__bridge id)kCGImageSourceShouldCache : @(YES),
(__bridge id)kCGImageSourceShouldAllowFloat : @(YES)
};

CGImageSourceRef imageSourceRef = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL);
CGImageRef imageRef = CGimageSourceCreateImageAtIndex(imageSourceRef, 0, (__bridge CFDictionaryRef)options);

UIImage *image = [UIImage imageWithCGImage:imageRef];

CFRelease(imageRef);
CFRelease(imageSourceRef);

0x05 Incrementally Reading an Image

前面的示例可以被扩展一下成为增量加载图像,这可以为加载特别大或远程图像提供一个更好的用户体验。

由于很多应用程序通过 HTTP 来加载图像,session task delegate 方法 URLSession:dataTask:didReceiveData: 是一个性能增益(performance gains)非常好的机会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask disReceiveData:(NSData *)data {
[self.mutableResponseData appendData:data];
CGImageSourceUpdateData(self.imageSourceRef, (__bridge CFDataRef)self.mutaleResponseData, [self.mutableResponseData length][dataTask countOfBytesExoectedToReceive]); // 原书这里的代码好像有点问题

if (CGSizeEqualToSize(self.imageSize, CGSizeZero)) {
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(self.imageSourceRef, 0, NULL);

if (properties) {
NSNumber *width = properties[(__bridge id)kCGImagePropertyPixelWidth];
NSNumber *height = properties[(__bridge id)kCGImagePropertyPixelHeight];

if (width && height) {
self.imageSize = CGSizeMake([width floatValue], [height floatValue]);
}
}
}

CGImageRef imageRef = CGImageSourceCreateImageAtIndex(self.imageSourceRef, 0, NULL);
UIImage *image = [UIImage imageWithCGImage:imageRef];
CFRelease(imageRef);

dispatch_async(dispatch_get_main_queue(), ^{
// delete or block callback to update with image
});
}

给定一个 CGImageSourceRef,它将会在请求开始加载时被初始化,这个 delegate 方法调用 CGImageSourceUpdateData 用响应数据缓冲区(response data buffer)更新。

如果已经加载了足够的数据来确定图像的最终尺寸,CGImageSourceCopyPropertiesAtIndex 可以检索该信息并对其进行高速缓存。从那时起,一个 delegate 或 block 回调将能够发送部分加载的图像以更新主线程上的 UI。

0x06 Image Metadata

在增量图像加载示例中,从其元数据中检索到图像的宽和高,以便可以正确地调整大小 – 即使在所有数据加载之前。

可以使用相同的方法检索图像元数据,诸如 GPS 数据(位置),相机 EXIF(透镜,曝光,快门速度等),或 IPTC(information suitable for publication,创作者和版权)。

元数据被分为几个不同的字典,可以用以下任意 key 来指定:

  • kCGImagePropertyTIFFDictionary
  • kCGImagePropertyGIFDictionary
  • kCGImagePropertyJFIFDictionary
  • kCGImagePropertyExifDictionary
  • kCGImagePropertyPNGDictionary
  • kCGImagePropertyIPTCDitionary
  • kCGImagePropertyGPSDictionary
  • kCGImagePropertyCIFFDictionary
  • kCGImageProperty8BIMDictionary
  • kCGImagePropertyDNGDictionary
  • kCGImagePropertyExifAuxDictionary

使用 Image I/O 检索图像元数据属性是相当不言自明的:一个调用 CGImageSourceCopyPropertiesAtIndex,从此刻开始它是在 CGImageProperty keys 上所有标准 NSDictionary 访问(it’s all standard NSDictionary access on CGImageProperty keys from there on out):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(self.imageSourceRef, 0, NULL);

NSDictionary *EXIF = properties[(__bridge id)kCGImagePropertyExifDictionary];
if (EXIF) {
NSString *Fnumber = EXIF[(__bridge id)kCGImagePropertyExifNumber];
NSString *exposure = EXIF[(__bridge id)kCGImagePropertyExifExposureTime];
NSString *ISO = EXIF[(__bridge id)kCGImagePropertyExifISOSpeedRatings];

NSLog(@"Shot Information: %@ %@ %@", Fnumber, exposure, ISO);
}

NSDictionary *GPS = properties[(__bridge id)kCGImagePropertyGPSDictionary];
if (GPS) {
NSString *latitude = GPS[(__bridge id)kCGImagePropertyGPSLatitude];
NSString *latitudeRef = GPS[(__bridge id)kCGImagePropertyGPSLatitudeRef];

NSString *longitude = GPS[(__bridge id)kCGImagePropertyGPSLongitude];
NSString *longitudeRef = GPS[(__bridge id)kCGImagePropertyGPSLongitudeRef];

NSLog(@"GPS: %@ %@ / %@ %@", latitude, latitudeRef, longitude, longitudeRef);
}