iOS图片-ImageIO介绍

介绍下iOS中ImageIO的概览与应用
Image I/O Programming Guide

介绍

ImageIO框架提供了读取与写入图片数据的基本方法,使用它可以直接获取到图片文件的内容数据,ImageIO具有很多特性 Mac平台上最快的图像解码器和编码器 逐步加载图像的能力`支持图像元数据有效缓存` ImageIO框架中包含6个头文件:

  1. ImageIO.CGImageAnimation
  2. ImageIO.CGImageDestination 负责写入图片数据。
  3. ImageIO.CGImageMetadata 图片文件元数据类
  4. ImageIO.CGImageProperties 定义了框架中使用的字符串常量和宏。
  5. ImageIO.CGImageSource 负责读取图片数据。
  6. ImageIO.ImageIOBase 预处理逻辑

获取支持的图片格式

ImageIO 框架了解大多数常见的图像文件格式,例如JPEG,JPEG2000,RAW,TIFF,BMP和PNG。并非每个平台都支持所有格式
通过 CGImageSourceCopyTypeIdentifiers 和 CGImageDestinationCopyTypeIdentifiers 可以看到支持的图片格式。

1
2
3
4
//返回Image IO 支持的统一类型标识符(UTI)数组作为图像源。
let mySourceTypes = CGImageSourceCopyTypeIdentifiers()
//返回Image IO 支持的统一类型标识符(UTI)的数组作为图像目标。
let myDestTypes = CGImageDestinationCopyTypeIdentifiers()

基本使用|获取图片 缩略图

在平时开发中,我们通常使用UIImage来读取图片,UIImage支持的图片包括png与jpg等,但是类似ico图标,UIImage默认是无法显示的,
可以通过ImageIO框架来在iOS系统中使用ico图标,示例如下

1
2
3
4
5
6
7
8
9
10
func getImage() -> UIImage? {
guard let url = Bundle.main.url(forResource: "image.ico", withExtension: nil) else{return nil}
//从URL创建图像源。
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else{return nil}
//获取图片
guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else{ return nil}
//获取缩略图
let thumb = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, nil)
return UIImage(cgImage: cgImage)
}

获取缩略图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func downsample(imageAt imageURL:URL,to pointSize:CGSize,scale:CGFloat)->UIImage{
let imageSourceOptions = [kCGImageSourceShouldCache:false] as CFDictionary
let imageSouce = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!

let maxDimensionInPixels = max(pointSize.width,pointSize.height) * scale
let downsampleOptions =
[
kCGImageSourceCreateThumbnailFromImageAlways:true,
kCGImageSourceShouldCacheImmediately:true,
kCGImageSourceCreateThumbnailWithTransform:true,
kCGImageSourceThumbnailMaxPixelSize:maxDimensionInPixels
] as CFDictionary

let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSouce, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}

渐进渲染大图

渐进式解码(Progressive Decoding),即不需要完整的图像流数据,允许解码部分帧(大部分情况下,会是图像的部分区域),对部分使用了渐进式编码的格式(参考:渐进式编码)),则更可以解码出相对模糊但完整的图像。比如说,JPEG支持三种方式的渐进式编码,包括Baseline,interlaced,以及progressive

使用ImageIO框架可以实现大图渐进渲染的效果,一般在对大图片进行网络请求时,可以获取一部分数据就加载一部分数据
创建一个空的CGImageSource,然后在每次收到数据的时候调用CGImageSourceUpdateData更新imageSource的数据,接着调用CGImageSourceCreateImageAtIndex获取最新的图片即可

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//demo使用计时器模拟
class ProgressiveLoadViewController : UIViewController{

deinit {
incrementTimer = nil
}

private var data = Data()
private var progress: Int = 0
private var incrementTimer: Timer? {
didSet {
oldValue?.invalidate()
}
}
private var cgImageSource = CGImageSourceCreateIncremental(nil)

override func viewDidLoad() {
guard let data = try? Data(contentsOf: URL(string: "some url")!) else {
print("failed to load image source")
return
}
self.data = data
self.progress = 0
//要渐进显示的图片
guard let cgImage = CGImageSourceCreateImageAtIndex(self.cgImageSource, 0, nil) else{
return
}
let uiImage = UIImage(cgImage: cgImage)
}

override func viewWillAppear(_ animated: Bool) {

self.incrementTimer = Timer.scheduledTimer(timeInterval: 0.25, target: self,
selector: #selector(self.incrementImage), userInfo: nil, repeats: true)
}

@objc func incrementImage(){
self.progress += 500
let chunk = self.data.prefix(self.progress)
if chunk.count == self.data.count {
self.incrementTimer = nil
}

CGImageSourceUpdateData(self.cgImageSource, Data(chunk) as CFData, chunk.count == self.data.count)

}
}

读取图像格式元数据

创建好CGImageSource之后,我们是可以立即解码。但是很多情况下,我们需要获取一些相关的图像信息,包括图像的格式,图像数量,EXIF元数据等。在真正解码之前,我们可以拿到这些数据,进行一些处理,之后再开始解码过程。

其中,这些信息可以直接在CGImageSource上获取:

  • 图像格式:CGImageSourceGetType
  • 图像数量(动图):CGImageSourceGetCount

其他的,需要通过获取属性列表来查询。对于图像容器的属性(EXIF等),我们需要使用CGImageSourceCopyProperties即可,然后根据不同的Key去获取对应的信息。

其实苹果还有一套CGImageSourceCopyMetadataAtIndex,对应的数据不是字典,而是一个CGImageMetadata,再通过其他方法去取。这套API使用起来也是可以的,读取数据和前者是完全兼容一致的,优点是能够进行自定义扩展(比如说你有非标准的图像信息想自己添加和删除)。一般来说使用前者就足够了。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
CGImageSourceCopyPropertiesAtIndex方法都会返回一个字典,字典中可能包含如下有意义的键:

//TIFF信息字典
const CFStringRef kCGImagePropertyTIFFDictionary;
/GIF信息字典
const CFStringRef kCGImagePropertyGIFDictionary;
//JFIF信息字典
const CFStringRef kCGImagePropertyJFIFDictionary;
//EXif信息字典
const CFStringRef kCGImagePropertyExifDictionary;
//PNG信息字典
const CFStringRef kCGImagePropertyPNGDictionary;
//IPTC信息字典
const CFStringRef kCGImagePropertyIPTCDictionary;
//GPS信息字典
const CFStringRef kCGImagePropertyGPSDictionary;
//原始信息字典
const CFStringRef kCGImagePropertyRawDictionary;
//CIFF信息字典
const CFStringRef kCGImagePropertyCIFFDictionary;
//佳能相机信息字典
const CFStringRef kCGImagePropertyMakerCanonDictionary;
//尼康相机信息字典
const CFStringRef kCGImagePropertyMakerNikonDictionary;
//柯尼卡相机信息字典
const CFStringRef kCGImagePropertyMakerMinoltaDictionary;
//富士相机信息字典
const CFStringRef kCGImagePropertyMakerFujiDictionary;
//奥林巴斯相机信息字典
const CFStringRef kCGImagePropertyMakerOlympusDictionary;
//宾得相机信息字典
const CFStringRef kCGImagePropertyMakerPentaxDictionary;
//对应Photoshop相片的信息字典
const CFStringRef kCGImageProperty8BIMDictionary;
//NDG信息字典
const CFStringRef kCGImagePropertyDNGDictionary ;
//ExifAux信息字典
const CFStringRef kCGImagePropertyExifAuxDictionary;
//OpenEXR信息字典
const CFStringRef kCGImagePropertyOpenEXRDictionary;
//Apple相机信息字典
const CFStringRef kCGImagePropertyMakerAppleDictionary ;
*/

显示GIF

  1. 首先使用ImageIO库中的CGImageSource加载Gif文件。
  2. 通过CGImageSource获取到Gif文件中的总的帧数,以及每一帧的显示时间。
  3. 通过CAKeyframeAnimation来完成Gif动画的播放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getGif(url:URL){
guard let source = CGImageSourceCreateWithURL(url as NSURL, nil) else {return;};
//获取gif中图片的个数
let count = CGImageSourceGetCount(source)

for index in 0...count {
let image = CGImageSourceCreateImageAtIndex(source, index, nil)
//获取图片信息
let info = CGImageSourceCopyProperties(source, nil) as! NSDictionary
let width = info[kCGImagePropertyWidth]
let height = info[kCGImagePropertyHeight]
let timeDic = info[kCGImagePropertyGIFDictionary]
}

}

创建动画图片

  1. 首先,它创建一对字典来保存动画属性。第一个字典指定动画PNG在停止到最后一帧之前应重复其动画的时间。第二个字典指定序列中每个帧使用的帧延迟。
  2. 创建图像目标之后,代码将设置目标图像的文件属性,然后一次添加一个帧。
  3. 最后,CGImageDestinationFinalize调用该方法以完成动画PNG。
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
26
27
28
29
30
31
func createAnimatePng(fileURL:CFURL,kUTTypePNG:CFString,imageForFrame){
let loopCount = 1
let frameCount = 60

var fileProperties = NSMutableDictionary()
fileProperties.setObject(kCGImagePropertyPNGDictionary, forKey: NSDictionary(dictionary: [kCGImagePropertyAPNGLoopCount: frameCount]))

var frameProperties = NSMutableDictionary()
frameProperties.setObject(kCGImagePropertyPNGDictionary, forKey: NSDictionary(dictionary: [kCGImagePropertyAPNGDelayTime: 1.0 / Double(frameCount)]))

guard let destination = CGImageDestinationCreateWithURL(fileURL, kUTTypePNG, frameCount, nil) else {
// Provide error handling here.
}

CGImageDestinationSetProperties(destination, fileProperties.copy() as? NSDictionary)

for i in 0..<frameCount {
autoreleasepool {
let radians = M_PI * 2.0 * Double(i) / Double(frameCount)
guard let image = imageForFrame(size: CGSize(width: 300, height: 300)) else {
return
}

CGImageDestinationAddImage(destination, image, frameProperties)
}
}

if !CGImageDestinationFinalize(destination) {
// Provide error handling here.
}
}

图片编码 | CGImageDestination

imageIO中使用CGImageDestination对静态图的编码,基本可以分为以下步骤:

  • 创建CGImageDestination
  • 添加图像格式元数据(可选)和CGImage
  • 编码得到NSData,清理

1. 创建CGImageDestination

CGImageDestination的创建也有三个接口,你需要提供一个输出的目标来输出解码后的数据。同时,由于编码需要提供文件格式,你需要指明对应编码的文件格式,用的是UTI Type。对于静态图来说,第三个参数的数量都写1即可。

  • CGImageDestinationCreateWithData:指定一个可变二进制数据作为输出
  • CGImageDestinationCreateWithURL:指定一个文件路径作为输出
  • CGImageDestinationCreateWithDataConsumer:指定一个DataConsumer作为输出

2. 添加图像格式元数据(可选)和CGImage

接下来就是添加图像了,由于CGImage只是包含基本的图像信息,很多额外信息比如说EXIF都已经丢失了,如果我们需要,可以添加对应的元信息。不像解码那样提供了两个API分别获取元信息和图像。使用的接口是CGImageDestinationAddImage。

当然,如果有自定义的元信息,可以通过另外的CGImageDestinationAddImageAndMetadata来添加CGImageMetadata,这个上面解码也说到过,这里就不解释了。

此外,还有一个ImageIO最强大的功能,叫做CGImageDestinationAddImageFromSource(这个东西可以媲美vImageConvert_AnyToAny,后续教程会谈到),这个能够从一个任意的CGImageSource,添加一个图像帧到任意一个CGImageDestination。这个一般的用途,就是专门给图像转换器用的,比如说从图像格式A,转换到图像格式B。我们不需要先解码到A的UIImage,再通过编码到B的NSData,直接在中间就进行了转换。能够极大地提升转换效率(Image/IO底层就是通过vImage,传的是Bitmap的引用,没有额外的消耗)。不过这篇教程侧重于Image/IO的编码和解码,转换可以自行参考处理,不再详细说明了。

3. 编码得到NSData,清理

当添加完成所有需要编码的CGImage之后,最后一步,就是进行编码,得到图像格式的数据。这里直接用一个方法CGImageDestinationFinalize即可,编码得到的数据,会写入最早初始化时提供的Data或者DataConsumer。