UIKit Images
UIKit提供了许多函数可以让我们操作Image,甚至我们可以仅通过代码的方式,获取一个UIImage。
UIImage *SwatchWithColor(UIColor *color, CGFloat side) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(side, side), YES, 0.0);
[color setFill];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
NOTO:
UIImage suports TIFF, JPEG, GIF, PNG, DIB(that is, BMP), ICO, CUR and XBM formats. You can load additional formats (like RAW) by using the ImageIO framework)
Image 的缩略图
制作Image的缩略图的代码很简单,核心代码是UIImage的方法drawRect:
UIImage *image = [UIImage imageNamed:@"myImage"];
[image drawInRect:destinationRect];
UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext();
而这里的难点在于,如何确定destinationRect。如果我们不做任何调整,直接使用目标rect的话,图像的比例就会失真。如果下图所示,图片的尺寸为高宽 : 2833 像素 1933像素, 当我们将其draw到一个矩形rect后,其高度就会压缩来适应:
乍看之下,似乎并不影响什么。但如果我们是在现实人脸等有长宽特征的图片是,就会感觉很奇怪。
要解决这种问题,可以使用我们在上一篇中提到的Fitting和Filling模式。下面是具体的代码:
UIImage *BuildThumbnail(UIImage *sourceImage, CGSize targetSize, BOOL useFitting){
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
CGRect targetRect = SizeMakeRect(targetSize);
CGRect naturalRect = CGSizeMake(.size = sourceImage.size);
// RectByFittingRect 和 RectByFillingRect的定义见上一篇博客
CGRect destinationRect = useFitting?
RectByFittingRect(nartualRect, targetRect):
RectByFillingRect(naturalRect, targetRect);
[sourceImage drawInRect:destinationRect];
UIImage *thumbinal = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return thumbinal;
}
CGRect SizeMakeRect(CGSize size)
{
return (CGRect){.size = size};
}
得出的结果如下所示:
图片裁剪
不同于缩略图,缩略图会将原始图的data压缩,而裁剪图片则会使用原始的分辨率,但仅是截取原始图片的一部分。
如下图所示,当我们截取雪貂的头部放大时,图片会变得模糊,因为其分辨率是一定的。
我们需要使用Quartz提供的
CGImageCreateWithImageInRect()
方法来裁剪图片。
注意,当我使用Core Graphics方法来裁剪时,可以利用方法CGRectIntegral()来调整裁剪图片的范围(以像素为单位),使得裁剪范围落在原始图片范围内。
//CGRectIntegral 用法
/*
将origin值向下调整到最近整数,size向上调整到最近整数,使生成的CGRect可以完全包含原来的CGRect.
*/
CGRect integralRect = CGRectIntegral(originalRect);
NSLog(@"integralRect = %@",NSStringFromCGRect(integralRect));
下面分别给出Quartz和UIKit两个版本的裁剪图片方法,注意,Quartz是以像素为单位的,而UIKit则是以逻辑单位点为单位。
Quartz :
UIImage *ExtractRectFromImage(UIImage *sourceImage, CGRect subRect) {
CGImageRef imageRef = CGImageCreateWithImageInRect(sourceImage.CGImage, subRect);
if (imageRef != NULL) {
UIImage *output = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
return output;
}
return nil;
}
UIKit:
UIImage *ExtractSubimageFromRect(UIImage *sourceImage, CGRect rect) {
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1);
CGRect destRect = CGRectMake(-rect.origin.x, -rect.origin.y, sourceImage.size.width, sourceImage.size.height);
[sourceImage drawInRect:destRect];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
灰度图
我们可以将图片的颜色信息完全抹去,仅留下灰度信息。称之为灰度图。
灰度图每一个像素占用一个字节(8 bits),没有透明度信息。
当我们需要创建一个灰度图时,需要先创建一个grayscale的color space。在这个color space中,你所添加的任何颜色,都会别Quartz自动转换为灰度强弱信息,而不会显示原始的颜色。创建灰度图的代码如下:
UIImage *GrayscaleVersionOfImage(UIImage *sourceImage) {
// 创建灰度颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
if (colorSpace == NULL) {
return nil;
}
int width = sourceImage.size.width;
int height = sourceImage.size.height;
// 创建 context:每一个像素 8 bits,没有透明度
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
8,
width, colorSpace,
(CGBitmapInfo)kCGImageAlphaNone);
if (context == NULL) {
return nil;
}
// 在灰度空间中,绘制图片
CGRect rect = SizeMakeRect(sourceImage.size);
CGContextDrawImage(context, rect, sourceImage.CGImage);
CGImageRef imageRef = CGBitmapContextCreateImage(context);// 等同于UICraphicsGetImageFromCurrentImageContext
CGContextRelease(context);
// 返回灰度图片
UIImage *output = [UIImage imageWithCGImage:imageRef];
CFRelease(imageRef);
return output;
}
图像水印
水印会损坏原始的图像信息,因此,在加了水印的图片中去除水印,是十分困难的。
下面是一个水印的例子:
这只是一个简单的水印效果,主要是blend了string和图片。
CGSize targetSize = CGSizeMake(1,2);
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
// 将原始图片draw到当前的context中
CGRect targetRect = SizeMakeRect(targetSize);
UIImage *sourceImage = [UIImage imageNamed:@"pronghorn.jpg"];
CGRect imgRect = RectByFillingRect(SizeMakeRect(sourceImage.size), targetRect);
[sourceImage drawInRect:imgRect];
// Rotate context,使得水印文字倾斜45°
CGPoint center = RectGetCenter(targetRect);
CGContextTranslateCTM(context, center.x, center.y);
CGContextRotateCTM(context, M_PI_4);
CGContextTranslateCTM(context, -center.x, -center.y);
// 创建水印内容
NSString *watermark = @"watermark";
UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:48];
CGSize size = [watermark sizeWithAttributes:@{NSFontAttributeName:font}];
CGRect stringRect = RectCenteredInRect(SizeMakeRect(size), targetRect);
// Draw 水印, 同时使用bleng将水印突出显示
CGContextSetBlendMode(context, kCGBlendModeDifference);
[watermark drawInRect:stringRect withAttributes:@{NSFontAttributeName:font, NSForegroundColorAttributeName:[UIColor whiteColor]}];
// 生成图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
Retrieving Image Data
尽管我们可以通过函数
UIImagePNGRepresentation()
UIImageJPEGRepresentation()
来获取NSData类型的信息,但是这些信息中包含了文件头,压缩信息等。
而当我们需要对一张图片进行处理时,我们需要单纯的byte-by-byte的图片信息。这里,我们展示了如何获取图像的字节信息并存储为NSData类型的。
转换的主要思路为:
1. 将图片draw到context中
2. 调用CGBitmapContextGetData()获取图像的bytes信息
NOTE: 我们对于CGBitmapContextCreate函数已经不再陌生,其函数原型为:
CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, space, bitmapInfo)
其中第一个参数data,表示我们为Context所分配的内存。我们有两种选择,当传入NULL的时候,Quartz会自动管理Context内存,而不需要手动dealloc。如果我们传入了data值,则需要我们手动删除内存。
其实,当我们调用CGBitmapContextGetData()的时候,就是获取创建Context时所设定的data。
NSData *BytesFromRGBImage(UIImage *sourceImage) {
if (!sourceImage) {
return nil;
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
return nil;
}
// 创建Context
int width = sourceImage.size.width;
int height = sourceImage.size.height;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (context == NULL) {
return nil;
}
// 将图片draw到当前context中
CGRect rect = (CGRect){.size = sourceImage.size};
CGContextDrawImage(const, rect, sourceImage.CGImage);
// 由bytes获取NSData
NSData *data = [NSData dataWithBytes:CGBitmapContextGetData(const) length:(width * height * 4)];
CGContextRelease(context);
return data;
}
由Bytes获取Images
由Bytes转换为UIImage通用是借助于
CGBitmapContextCreate
这里我们将第一个参数传入,即图片的data。这就告诉Quartz,不要在自动为我们分配内存,而是使用我们指定的内存。
现在,我们可以在bytes和image直接相互转换了,也就可以使得image可以修改了。
UIImage *ImageFromBytes(NSData *data, CGSize targetSize) {
int width = targetSize.width;
int height = targetSize.height;
if (data.length < (width * height * 4)) {
NSLog(@"Error: Got %d bytes. Expected %d bytes",
data.length, width * height * 4);
return nil;
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) {
return nil;
}
// 为bitmap创建context
Byte *bytes = (Byte *)data.bytes;
CGContextRef context = CGBitmapContextCreate(bytes,
width, height,
8,
width * 4,
colorSpace,
(CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (context == NULL) {
return nil;
}
// 将data转换为image
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image = [UIImage imageWithCGImage:imageRef];
// clean up
CGContextRelease(context);
CFRelease(imageRef);
return image;
}
Image的autolayout
我们在iOS中,通过Auto Layout来对齐各个View。
但有一个我们大家经常忽略的事实:Auto Layout并不是通过View的Frame来对齐的,而是通过Alignment rectangle。
通常,我们不必在意这个细节,因为大多数情况下,Frame和Alignment rectangle是相等的。
但是当我们的Image中包含阴影,高光等装饰性元素时,当我们想要将Image加载到ImageView中并利用Auto Layout对齐时,情况往往变得不尽如人意。(注意这里说的阴影的效果不是指我们通过layer代码添加的元素,而是指图片自带的内容)
例如,我们想居中对齐一个绿色矩形,带有黑色阴影的图片, 默认的Auto Layout居中效果如下:
可以明显的看到,绿色矩形并没有居中。这是为什么呢?我们继续看。
Debugging Alignment Rectangles
由于Auto Layout是根据Alignment Rectangle排列View的,因此我们可以查看Alignment Rectangle 来debug约束。
具体做法为在XCode中,选择Edit scheme,并在launch argument中输入
-UIViewShowAlignmentRects YES
可以看到,Alignment rectangle被黄线框了出来。可以看到,Alignment rectangle是和imageView的frame相同的,又因为阴影和绿色矩形在同一张图片,因此,绿色矩形就没有显示到正中央啦。
为了修正这种错误,我们需要将Alignment rectangle仅围绕绿色矩形。我们可以利用UIImage的方法
- (UIImage *)imageWithAlignmentRectInsets:(UIEdgeInsets)alignmentInsets
来约定UIImage的Alignment Rectangle。这里我首先看一下矩形和阴影的inset:
知道了这个间隙,我们就通过函数
- (UIImage *)imageWithAlignmentRectInsets:(UIEdgeInsets)alignmentInsets
以原始图片为蓝本,创建一个修正了Alignment Rectangle的新image:
UIImage *newImage = [image imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 30, 30)];
将newImage加载如UIImageView中,在debug,则看到黄色的框现在是紧紧包裹着绿色矩形啦,这样,绿色矩形得到了居中。
示例代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 调整图片的Alignment Rectangle
UIImage *image = [self createImage];
// 居中imageView
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:imageView];
[imageView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
[imageView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
}
- (UIImage *)createImage {
UIGraphicsBeginImageContext(CGSizeMake(250, 70));
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(50, 20, 200, 50)];
[[UIColor blackColor] setFill];
[path fill];
path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 200, 50)];
[[UIColor greenColor] setFill];
[path fill];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIImage *newImage = [image imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 20, 50)];
UIGraphicsEndImageContext();
return newImage;
}
可拉伸的图片
当我们使用图片时,如果要显示的区域和原始图片尺寸不符时,为了填充显示区域,我们需要拉伸图片。如下图所示
这时,我们可以通过UIImage的方法:
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode
来指定图片可以拉伸的部位。
具体可以参考资料:
resizableImageWithCapInsets:方法的探析
View的背景图片
一般的,我们可以设置UIView的background color,却不能够设置背景图片(UIImageView除外)。在这里,介绍两种方法来设置普通View的背景图片:
- UIColor的 colorWithPatternImage方法
self.view.backgroundColor=[UIColor colorWithPatternImage:[UIImage imageNamed:@"a"]];
- UILayer的contents属性
self.view.layer.contents = (id)image.CGImage;
这里推荐使用第二种方法,因为第一种方法会占用大量的内存。
PS:
imageNamed: 这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。因此imageNamed的优点是当加载时会缓存图片。所以当图片会频繁的使用时,那么用imageNamed的方法会比较好。例如:你需要在 一个TableView里的TableViewCell里都加载同样一个图标,那么用imageNamed加载图像效率很高。系统会把那个图标Cache到内存,在TableViewCell里每次利用那个图 像的时候,只会把图片指针指向同一块内存。正是因此使用imageNamed会缓存图片,即将图片的数据放在内存中,iOS的内存非常珍贵并且在内存消耗过大时,会强制释放内存,即会遇到memory warnings。而在iOS系统里面释放图像的内存是一件比较麻烦的事情,有可能会造成内存泄漏。例如:当一 个UIView对象的animationImages是一个装有UIImage对象动态数组NSMutableArray,并进行逐帧动画。当使用imageNamed的方式加载图像到一个动态数组NSMutableArray,这将会很有可能造成内存泄露。原因很显然的。
imageWithContentsOfFile:仅加载图片,图像数据不会缓存。因此对于较大的图片以及使用情况较少时,那就可以用该方法,降低内存消耗。